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
108 changed files with 17511 additions and 29092 deletions

View File

@@ -42,7 +42,7 @@ jobs:
defaults: defaults:
run: run:
# Windows uses cmd for better conda/mamba compatibility, Unix uses bash # 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: steps:
- name: Check if platform should be built - name: Check if platform should be built
@@ -73,6 +73,7 @@ jobs:
channels: conda-forge,robostack-staging,uni-lab,defaults channels: conda-forge,robostack-staging,uni-lab,defaults
channel-priority: flexible channel-priority: flexible
activate-environment: unilab activate-environment: unilab
auto-activate-base: true
auto-update-conda: false auto-update-conda: false
show-channel-urls: true show-channel-urls: true
@@ -81,7 +82,7 @@ jobs:
run: | run: |
echo Installing unilabos and dependencies to unilab environment... echo Installing unilabos and dependencies to unilab environment...
echo Using mamba for faster and more reliable dependency resolution... echo Using mamba for faster and more reliable dependency resolution...
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) - name: Install conda-pack, unilabos and dependencies (Unix)
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64' if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
@@ -89,15 +90,15 @@ jobs:
run: | run: |
echo "Installing unilabos and dependencies to unilab environment..." echo "Installing unilabos and dependencies to unilab environment..."
echo "Using mamba for faster and more reliable dependency resolution..." echo "Using mamba for faster and more reliable dependency resolution..."
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) - name: Get latest ros-humble-unilabos-msgs version (Windows)
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64' if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
id: msgs_version_win id: msgs_version_win
run: | run: |
echo Checking installed ros-humble-unilabos-msgs version... echo Checking installed ros-humble-unilabos-msgs version...
conda list -n unilab ros-humble-unilabos-msgs conda list 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 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_version=%VERSION% >> %GITHUB_OUTPUT%
echo Installed ros-humble-unilabos-msgs version: %VERSION% echo Installed ros-humble-unilabos-msgs version: %VERSION%
@@ -107,7 +108,7 @@ jobs:
shell: bash shell: bash
run: | run: |
echo "Checking installed ros-humble-unilabos-msgs version..." 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_version=$VERSION" >> $GITHUB_OUTPUT
echo "Installed ros-humble-unilabos-msgs version: $VERSION" 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 mamba search ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge || echo Search completed
echo. echo.
echo Updating ros-humble-unilabos-msgs to latest version... 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) - name: Check for newer ros-humble-unilabos-msgs (Unix)
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64' if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
@@ -128,65 +129,65 @@ jobs:
mamba search ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge || echo "Search completed" mamba search ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge || echo "Search completed"
echo "" echo ""
echo "Updating ros-humble-unilabos-msgs to latest version..." 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) - name: Install latest unilabos from source (Windows)
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64' if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
run: | run: |
echo Uninstalling existing unilabos... 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 }})... echo Installing unilabos from source (branch: ${{ github.event.inputs.branch }})...
mamba run -n unilab pip install . pip install .
echo Verifying installation... echo Verifying installation...
mamba run -n unilab pip show unilabos pip show unilabos
- name: Install latest unilabos from source (Unix) - name: Install latest unilabos from source (Unix)
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64' if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
shell: bash shell: bash
run: | run: |
echo "Uninstalling existing unilabos..." 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 }})..." echo "Installing unilabos from source (branch: ${{ github.event.inputs.branch }})..."
mamba run -n unilab pip install . pip install .
echo "Verifying installation..." echo "Verifying installation..."
mamba run -n unilab pip show unilabos pip show unilabos
- name: Display environment info (Windows) - name: Display environment info (Windows)
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64' if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
run: | run: |
echo === Environment Information === echo === Environment Information ===
mamba env list conda env list
echo. echo.
echo === Installed Packages === 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.
echo === Python Packages === 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) - name: Display environment info (Unix)
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64' if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
shell: bash shell: bash
run: | run: |
echo "=== Environment Information ===" echo "=== Environment Information ==="
mamba env list conda env list
echo "" echo ""
echo "=== Installed Packages ===" 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 ""
echo "=== Python Packages ===" 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) - name: Verify environment integrity (Windows)
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64' if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
run: | run: |
echo Verifying Python version... 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... 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... 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... 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! echo Environment verification complete!
- name: Verify environment integrity (Unix) - name: Verify environment integrity (Unix)
@@ -194,20 +195,20 @@ jobs:
shell: bash shell: bash
run: | run: |
echo "Verifying Python version..." 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..." 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..." 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..." 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!" echo "Environment verification complete!"
- name: Pack conda environment (Windows) - name: Pack conda environment (Windows)
if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64' if: steps.should_build.outputs.should_build == 'true' && matrix.platform == 'win-64'
run: | run: |
echo Packing unilab environment with conda-pack... 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: echo Pack file created:
dir unilab-env-${{ matrix.platform }}.tar.gz dir unilab-env-${{ matrix.platform }}.tar.gz
@@ -216,7 +217,6 @@ jobs:
shell: bash shell: bash
run: | run: |
echo "Packing unilab environment with conda-pack..." 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 conda pack -n unilab -o unilab-env-${{ matrix.platform }}.tar.gz --ignore-missing-files
echo "Pack file created:" echo "Pack file created:"
ls -lh unilab-env-${{ matrix.platform }}.tar.gz ls -lh unilab-env-${{ matrix.platform }}.tar.gz
@@ -242,10 +242,6 @@ jobs:
echo Adding: verify_installation.py echo Adding: verify_installation.py
copy scripts\verify_installation.py dist-package\ 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 rem Create README using Python script
echo Creating: README.txt echo Creating: README.txt
python scripts\create_readme.py ${{ matrix.platform }} ${{ github.event.inputs.branch }} dist-package\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" echo "Adding: verify_installation.py"
cp scripts/verify_installation.py dist-package/ 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 # Create README using Python script
echo "Creating: README.txt" echo "Creating: README.txt"
python scripts/create_readme.py ${{ matrix.platform }} ${{ github.event.inputs.branch }} dist-package/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/ ls -lh dist-package/
echo "" 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 - name: Upload distribution package
if: steps.should_build.outputs.should_build == 'true' if: steps.should_build.outputs.should_build == 'true'
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
@@ -333,8 +365,12 @@ jobs:
echo "Distribution package contents:" echo "Distribution package contents:"
ls -lh dist-package/ ls -lh dist-package/
echo "" 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 "Artifact name: unilab-pack-${{ matrix.platform }}-${{ github.event.inputs.branch }}"
echo "" echo ""
echo "After download:" 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 "==========================================" echo "=========================================="

View File

@@ -39,39 +39,24 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
ref: ${{ github.event.inputs.branch || github.ref }} ref: ${{ github.event.inputs.branch || github.ref }}
fetch-depth: 0
- name: Setup Miniforge (with mamba) - name: Setup Python environment
uses: conda-incubator/setup-miniconda@v3 uses: actions/setup-python@v5
with: with:
miniforge-version: latest python-version: '3.10'
use-mamba: true
python-version: '3.11.11'
channels: conda-forge,robostack-staging,uni-lab,defaults
channel-priority: flexible
activate-environment: unilab
auto-update-conda: false
show-channel-urls: true
- name: Install unilabos and dependencies - name: Install system dependencies
run: | run: |
echo "Installing unilabos and dependencies to unilab environment..." sudo apt-get update
echo "Using mamba for faster and more reliable dependency resolution..." sudo apt-get install -y pandoc
mamba install -n unilab uni-lab::unilabos -c uni-lab -c robostack-staging -c conda-forge -y
- name: Install latest unilabos from source - name: Install Python dependencies
run: | run: |
echo "Uninstalling existing unilabos..." python -m pip install --upgrade pip
mamba run -n unilab pip uninstall unilabos -y || echo "unilabos not installed via pip" # Install package in development mode to get version info
echo "Installing unilabos from source..." pip install -e .
mamba run -n unilab pip install . # Install documentation dependencies
echo "Verifying installation..." pip install -r docs/requirements.txt
mamba run -n unilab pip show unilabos
- name: Install documentation dependencies
run: |
echo "Installing documentation build dependencies..."
mamba run -n unilab pip install -r docs/requirements.txt
- name: Setup Pages - name: Setup Pages
id: pages id: pages
@@ -83,8 +68,8 @@ jobs:
cd docs cd docs
# Clean previous builds # Clean previous builds
rm -rf _build rm -rf _build
# Build HTML documentation in conda environment # Build HTML documentation
mamba run -n unilab python -m sphinx -b html . _build/html -v python -m sphinx -b html . _build/html -v
- name: Check build results - name: Check build results
run: | run: |

View File

@@ -31,7 +31,7 @@ Join the [Intelligent Organic Chemistry Synthesis Competition](https://bohrium.d
Detailed documentation can be found at: 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 ## Quick Start
@@ -55,7 +55,7 @@ pip install .
3. Start Uni-Lab System: 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 ## 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 系统: 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)
## 消息格式 ## 消息格式

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -23,7 +23,7 @@ extensions = [
"myst_parser", "myst_parser",
"sphinx.ext.autodoc", "sphinx.ext.autodoc",
"sphinx.ext.napoleon", # 如果您使用 Google 或 NumPy 风格的 docstrings "sphinx.ext.napoleon", # 如果您使用 Google 或 NumPy 风格的 docstrings
"sphinx_rtd_theme", "sphinx_rtd_theme"
] ]
source_suffix = { source_suffix = {

View File

@@ -1,68 +0,0 @@
{
"nodes": [
{
"id": "bioyond_cell_workstation",
"name": "配液分液工站",
"parent": null,
"children": [
"YB_Bioyond_Deck"
],
"type": "device",
"class": "bioyond_cell",
"config": {
"deck": {
"data": {
"_resource_child_name": "YB_Bioyond_Deck",
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_YB_Deck"
}
},
"protocol_type": []
},
"data": {}
},
{
"id": "YB_Bioyond_Deck",
"name": "YB_Bioyond_Deck",
"children": [],
"parent": "bioyond_cell_workstation",
"type": "deck",
"class": "BIOYOND_YB_Deck",
"position": {
"x": 0,
"y": 0,
"z": 0
},
"config": {
"type": "BIOYOND_YB_Deck",
"setup": true,
"rotation": {
"x": 0,
"y": 0,
"z": 0,
"type": "Rotation"
}
},
"data": {}
},
{
"id": "BatteryStation",
"name": "扣电工作站",
"children": [
"coin_cell_deck"
],
"parent": null,
"type": "device",
"class": "coincellassemblyworkstation_device",
"position": {
"x": 600,
"y": 400,
"z": 0
},
"config": {
"debug_mode": false,
"protocol_type": []
}
}
],
"links": []
}

View File

@@ -172,7 +172,7 @@ Examples:
with open(output_path, "w", encoding="utf-8") as f: with open(output_path, "w", encoding="utf-8") as f:
f.write(readme_content) 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" Platform: {args.platform}")
print(f" Branch: {args.branch}") 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. Run this script after installing the conda-pack environment to ensure everything works.
Usage: Usage:
python verify_installation.py [--auto-install] python verify_installation.py
Options:
--auto-install Automatically install missing packages
Or in the conda environment: Or in the conda environment:
conda activate unilab conda activate unilab
@@ -20,15 +17,14 @@ Usage:
import sys import sys
import os import os
import argparse
# IMPORTANT: Set UTF-8 encoding BEFORE any other imports # IMPORTANT: Set UTF-8 encoding BEFORE any other imports
# This ensures all subsequent imports (including unilabos) can output UTF-8 characters # This ensures all subsequent imports (including unilabos) can output UTF-8 characters
if sys.platform == "win32": if sys.platform == "win32":
# Method 1: Reconfigure stdout/stderr to use UTF-8 with error handling # Method 1: Reconfigure stdout/stderr to use UTF-8 with error handling
try: try:
sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore sys.stdout.reconfigure(encoding="utf-8", errors="replace")
sys.stderr.reconfigure(encoding="utf-8", errors="replace") # type: ignore sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except (AttributeError, OSError): except (AttributeError, OSError):
pass pass
@@ -53,7 +49,7 @@ CHECK_MARK = "[OK]"
CROSS_MARK = "[FAIL]" 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. Check if a package can be imported.
@@ -91,25 +87,9 @@ def check_python_version() -> bool:
def main(): def main():
"""Run all verification checks.""" """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("=" * 60)
print("UniLabOS Installation Verification") print("UniLabOS Installation Verification")
print("=" * 60) print("=" * 60)
if args.auto_install:
print("Mode: Auto-install missing packages")
else:
print("Mode: Verification only")
print() print()
all_passed = True all_passed = True
@@ -133,16 +113,14 @@ def main():
print(f" {CHECK_MARK} UniLabOS installed") 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 # 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: if env_check_passed:
print(f" {CHECK_MARK} All required packages available") print(f" {CHECK_MARK} All required packages available")
else: else:
print(f" {CROSS_MARK} Some optional packages are missing") 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: except ImportError:
print(f" {CROSS_MARK} UniLabOS not installed") print(f" {CROSS_MARK} UniLabOS not installed")
all_passed = False 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

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

View File

@@ -8,7 +8,7 @@
], ],
"parent": null, "parent": null,
"type": "device", "type": "device",
"class": "bioyond_dispensing_station", "class": "dispensing_station.bioyond",
"config": { "config": {
"config": { "config": {
"api_key": "DE9BDDA0", "api_key": "DE9BDDA0",
@@ -20,6 +20,13 @@
"_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck" "_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": [] "protocol_type": []
}, },
"data": {} "data": {}
@@ -50,4 +57,4 @@
"data": {} "data": {}
} }
] ]
} }

View File

@@ -10,7 +10,7 @@
"type": "device", "type": "device",
"class": "reaction_station.bioyond", "class": "reaction_station.bioyond",
"config": { "config": {
"config": { "bioyond_config": {
"api_key": "DE9BDDA0", "api_key": "DE9BDDA0",
"api_host": "http://192.168.1.200:44402", "api_host": "http://192.168.1.200:44402",
"workflow_mappings": { "workflow_mappings": {
@@ -19,18 +19,14 @@
"Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6", "Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6",
"Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47", "Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47",
"Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046", "Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046",
"Liquid_feeding(titration)": "3a16082a-96ac-0449-446a-4ed39f3365b6", "Liquid_feeding(titration)": "3a160824-0665-01ed-285a-51ef817a9046",
"liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784", "Liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784",
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a" "Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
}, },
"material_type_mappings": { "material_type_mappings": {
"烧杯": ["YB_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"], "烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
"试剂瓶": ["YB_1BottleCarrier", ""], "试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
"样品板": ["YB_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"], "样品板": "BIOYOND_PolymerStation_6VialCarrier"
"分装板": ["YB_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"],
"样品瓶": ["YB_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"],
"90%分装小瓶": ["YB_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"],
"10%分装小瓶": ["YB_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"]
} }
}, },
"deck": { "deck": {
@@ -46,6 +42,7 @@
{ {
"id": "Bioyond_Deck", "id": "Bioyond_Deck",
"name": "Bioyond_Deck", "name": "Bioyond_Deck",
"sample_id": null,
"children": [ "children": [
], ],
"parent": "reaction_station_bioyond", "parent": "reaction_station_bioyond",

View File

@@ -24,9 +24,9 @@
"Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a" "Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a"
}, },
"material_type_mappings": { "material_type_mappings": {
"烧杯": "YB_1FlaskCarrier", "烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
"试剂瓶": "YB_1BottleCarrier", "试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
"样品板": "YB_6VialCarrier" "样品板": "BIOYOND_PolymerStation_6VialCarrier"
} }
}, },
"deck": { "deck": {

View File

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

View File

@@ -1,52 +0,0 @@
[
{
"id": "3a1d377b-299d-d0f2-ced9-48257f60dfad",
"typeName": "加样头(大)",
"code": "0005-00145",
"barCode": "",
"name": "LiDFOB",
"quantity": 9999.0,
"lockQuantity": 0.0,
"unit": "个",
"status": 1,
"isUse": false,
"locations": [
{
"id": "3a19da56-1379-ff7c-1745-07e200b44ce2",
"whid": "3a19da56-1378-613b-29f2-871e1a287aa5",
"whName": "粉末加样头堆栈",
"code": "0005-0001",
"x": 1,
"y": 1,
"z": 1,
"quantity": 0
}
],
"detail": []
},
{
"id": "3a1d377b-6a81-6a7e-147c-f89f6463656d",
"typeName": "液",
"code": "0006-00141",
"barCode": "",
"name": "EMC",
"quantity": 99999.0,
"lockQuantity": 0.0,
"unit": "g",
"status": 1,
"isUse": false,
"locations": [
{
"id": "3a1baa20-a7b1-c665-8b9c-d8099d07d2f6",
"whid": "3a1baa20-a7b0-5c19-8844-5de8924d4e78",
"whName": "4号手套箱内部堆栈",
"code": "0015-0001",
"x": 1,
"y": 1,
"z": 1,
"quantity": 0
}
],
"detail": []
}
]

View File

@@ -1,99 +0,0 @@
{
"typeId": "3a190c8b-3284-af78-d29f-9a69463ad047",
"code": "",
"barCode": "",
"name": "test",
"unit": "",
"parameters": "{}",
"quantity": "",
"details": [
{
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
"code": "",
"name": "配液瓶(小)11",
"quantity": "1",
"x": 1,
"y": 1,
"z": 1,
"unit": "",
"parameters": "{}"
},
{
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
"code": "",
"name": "配液瓶(小)21",
"quantity": "1",
"x": 2,
"y": 1,
"z": 1,
"unit": "",
"parameters": "{}"
},
{
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
"code": "",
"name": "配液瓶(小)12",
"quantity": "1",
"x": 1,
"y": 2,
"z": 1,
"unit": "",
"parameters": "{}"
},
{
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
"code": "",
"name": "配液瓶(小)22",
"quantity": "1",
"x": 2,
"y": 2,
"z": 1,
"unit": "",
"parameters": "{}"
},
{
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
"code": "",
"name": "配液瓶(小)13",
"quantity": "1",
"x": 1,
"y": 3,
"z": 1,
"unit": "",
"parameters": "{}"
},
{
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
"code": "",
"name": "配液瓶(小)23",
"quantity": "1",
"x": 2,
"y": 3,
"z": 1,
"unit": "",
"parameters": "{}"
},
{
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
"code": "",
"name": "配液瓶(小)14",
"quantity": "1",
"x": 1,
"y": 4,
"z": 1,
"unit": "",
"parameters": "{}"
},
{
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb",
"code": "",
"name": "配液瓶(小)24",
"quantity": "1",
"x": 2,
"y": 4,
"z": 1,
"unit": "",
"parameters": "{}"
}
]
}

View File

@@ -1,148 +0,0 @@
[
{
"id": "3a1d4c14-a9fb-d7dc-9e96-7a3ad6e50219",
"typeName": "配液瓶(小)板",
"code": "0001-00093",
"barCode": "",
"name": "test",
"quantity": 2.0,
"lockQuantity": 0.0,
"unit": "块",
"status": 1,
"isUse": false,
"locations": [
{
"id": "3a19deae-2c7a-36f5-5e41-02c5b66feaea",
"whid": "3a19deae-2c79-05a3-9c76-8e6760424841",
"whName": "手动堆栈",
"code": "1",
"x": 1,
"y": 1,
"z": 1,
"quantity": 0
}
],
"detail": [
{
"id": "3a1d4c14-a9fc-1daa-71fa-146cb1ccb930",
"detailMaterialId": "3a1d4c14-a9fc-4f38-4c48-68486c391c42",
"code": "0001-00093 - 05",
"name": "配液瓶(小)",
"quantity": "1",
"lockQuantity": "0",
"unit": "个",
"x": 1,
"y": 3,
"z": 1,
"associateId": null,
"typeName": "配液瓶(小)",
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
},
{
"id": "3a1d4c14-a9fc-3659-ea61-cd587da9e131",
"detailMaterialId": "3a1d4c14-a9fc-018f-93e5-c49343d37758",
"code": "0001-00093 - 08",
"name": "配液瓶(小)",
"quantity": "1",
"lockQuantity": "0",
"unit": "个",
"x": 2,
"y": 4,
"z": 1,
"associateId": null,
"typeName": "配液瓶(小)",
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
},
{
"id": "3a1d4c14-a9fc-3f94-de83-979d2646e313",
"detailMaterialId": "3a1d4c14-a9fc-9987-c0ef-4b7cbad49e6b",
"code": "0001-00093 - 01",
"name": "配液瓶(小)",
"quantity": "1",
"lockQuantity": "0",
"unit": "个",
"x": 1,
"y": 1,
"z": 1,
"associateId": null,
"typeName": "配液瓶(小)",
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
},
{
"id": "3a1d4c14-a9fc-8c35-6b25-913b11dbaf4e",
"detailMaterialId": "3a1d4c14-a9fc-9a83-865b-0c26ea5e8cc4",
"code": "0001-00093 - 03",
"name": "配液瓶(小)",
"quantity": "1",
"lockQuantity": "0",
"unit": "个",
"x": 1,
"y": 2,
"z": 1,
"associateId": null,
"typeName": "配液瓶(小)",
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
},
{
"id": "3a1d4c14-a9fc-b41f-e968-64953bfddccd",
"detailMaterialId": "3a1d4c14-a9fc-daf7-9d64-e5ec8d3ae0e2",
"code": "0001-00093 - 07",
"name": "配液瓶(小)",
"quantity": "1",
"lockQuantity": "0",
"unit": "个",
"x": 1,
"y": 4,
"z": 1,
"associateId": null,
"typeName": "配液瓶(小)",
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
},
{
"id": "3a1d4c14-a9fc-c20f-c26e-b1bb2cdc3bca",
"detailMaterialId": "3a1d4c14-a9fc-673b-ac83-aaaf71287f1f",
"code": "0001-00093 - 06",
"name": "配液瓶(小)",
"quantity": "1",
"lockQuantity": "0",
"unit": "个",
"x": 2,
"y": 3,
"z": 1,
"associateId": null,
"typeName": "配液瓶(小)",
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
},
{
"id": "3a1d4c14-a9fc-cf21-059c-fde361d82b6f",
"detailMaterialId": "3a1d4c14-a9fc-25b1-e736-6b0d8dac0fae",
"code": "0001-00093 - 02",
"name": "配液瓶(小)",
"quantity": "1",
"lockQuantity": "0",
"unit": "个",
"x": 2,
"y": 1,
"z": 1,
"associateId": null,
"typeName": "配液瓶(小)",
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
},
{
"id": "3a1d4c14-a9fc-d732-2b93-9b2bd2bf581b",
"detailMaterialId": "3a1d4c14-a9fc-7f5d-b6b6-8bcb2e15f320",
"code": "0001-00093 - 04",
"name": "配液瓶(小)",
"quantity": "1",
"lockQuantity": "0",
"unit": "个",
"x": 2,
"y": 2,
"z": 1,
"associateId": null,
"typeName": "配液瓶(小)",
"typeId": "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"
}
]
}
]

View File

@@ -1,7 +1,7 @@
import pytest import pytest
from unilabos.resources.bioyond.bottle_carriers import BIOYOND_Electrolyte_6VialCarrier, BIOYOND_Electrolyte_1BottleCarrier from unilabos.resources.bioyond.bottle_carriers import BIOYOND_Electrolyte_6VialCarrier, BIOYOND_Electrolyte_1BottleCarrier
from unilabos.resources.bioyond.bottles import YB_Solid_Vial, YB_Solution_Beaker, YB_Reagent_Bottle from unilabos.resources.bioyond.bottles import BIOYOND_PolymerStation_Solid_Vial, BIOYOND_PolymerStation_Solution_Beaker, BIOYOND_PolymerStation_Reagent_Bottle
def test_bottle_carrier() -> "BottleCarrier": def test_bottle_carrier() -> "BottleCarrier":
@@ -16,9 +16,9 @@ def test_bottle_carrier() -> "BottleCarrier":
print(f"1烧杯载架: {beaker_carrier.name}, 位置数: {len(beaker_carrier.sites)}") print(f"1烧杯载架: {beaker_carrier.name}, 位置数: {len(beaker_carrier.sites)}")
# 创建瓶子和烧杯 # 创建瓶子和烧杯
powder_bottle = YB_Solid_Vial("powder_bottle_01") powder_bottle = BIOYOND_PolymerStation_Solid_Vial("powder_bottle_01")
solution_beaker = YB_Solution_Beaker("solution_beaker_01") solution_beaker = BIOYOND_PolymerStation_Solution_Beaker("solution_beaker_01")
reagent_bottle = YB_Reagent_Bottle("reagent_bottle_01") reagent_bottle = BIOYOND_PolymerStation_Reagent_Bottle("reagent_bottle_01")
print(f"\n创建的物料:") print(f"\n创建的物料:")
print(f"粉末瓶: {powder_bottle.name} - {powder_bottle.diameter}mm x {powder_bottle.height}mm, {powder_bottle.max_volume}μL") print(f"粉末瓶: {powder_bottle.name} - {powder_bottle.diameter}mm x {powder_bottle.height}mm, {powder_bottle.max_volume}μL")

View File

@@ -12,13 +12,23 @@ lab_registry.setup()
type_mapping = { type_mapping = {
"烧杯": ("YB_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"), "烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
"试剂瓶": ("YB_1BottleCarrier", ""), "试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
"样品板": ("YB_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"), "样品板": "BIOYOND_PolymerStation_6StockCarrier",
"分装板": ("YB_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"), "分装板": "BIOYOND_PolymerStation_6VialCarrier",
"样品瓶": ("YB_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"), "样品瓶": "BIOYOND_PolymerStation_Solid_Stock",
"90%分装小瓶": ("YB_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"), "90%分装小瓶": "BIOYOND_PolymerStation_Solid_Vial",
"10%分装小瓶": ("YB_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"), "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,75 +0,0 @@
from ast import If
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
from unilabos.resources.bioyond.decks import YB_Deck
lab_registry.setup()
type_mapping = {
"加样头(大)": ("YB_jia_yang_tou_da", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
"": ("YB_1BottleCarrier", "3a190ca1-2add-2b23-f8e1-bbd348b7f790"),
"配液瓶(小)板": ("YB_peiyepingxiaoban", "3a190c8b-3284-af78-d29f-9a69463ad047"),
"配液瓶(小)": ("YB_pei_ye_xiao_Bottler", "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"),
}
@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() -> list[dict]:
# 直接加载 bioyond_materials_reaction.json 文件
current_dir = os.path.dirname(os.path.abspath(__file__))
json_path = os.path.join(current_dir, "test.json")
with open(json_path, "r", encoding="utf-8") as f:
materials = json.load(f)
deck = YB_Deck("test_deck")
output = resource_bioyond_to_plr(materials, type_mapping=type_mapping, deck=deck)
print(output)
# 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)
if __name__ == "__main__":
test_resourcetreeset_from_plr()

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

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

View File

@@ -11,14 +11,18 @@ from typing import Dict, Any, List
import networkx as nx import networkx as nx
import yaml import yaml
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet, ResourceDict
# 首先添加项目根目录到路径 # 首先添加项目根目录到路径
current_dir = os.path.dirname(os.path.abspath(__file__)) current_dir = os.path.dirname(os.path.abspath(__file__))
unilabos_dir = os.path.dirname(os.path.dirname(current_dir)) unilabos_dir = os.path.dirname(os.path.dirname(current_dir))
if unilabos_dir not in sys.path: if unilabos_dir not in sys.path:
sys.path.append(unilabos_dir) sys.path.append(unilabos_dir)
from unilabos.utils.banner_print import print_status, print_unilab_banner
from unilabos.config.config import load_config, BasicConfig, HTTPConfig from unilabos.config.config import load_config, BasicConfig, HTTPConfig
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): def load_config_from_file(config_path):
if config_path is None: if config_path is None:
@@ -180,7 +184,6 @@ def main():
working_dir = os.path.abspath(os.getcwd()) working_dir = os.path.abspath(os.getcwd())
else: else:
working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data")) working_dir = os.path.abspath(os.path.join(os.getcwd(), "unilabos_data"))
if args_dict.get("working_dir"): if args_dict.get("working_dir"):
working_dir = args_dict.get("working_dir", "") working_dir = args_dict.get("working_dir", "")
if config_path and not os.path.exists(config_path): if config_path and not os.path.exists(config_path):
@@ -212,14 +215,6 @@ def main():
# 加载配置文件 # 加载配置文件
print_status(f"当前工作目录为 {working_dir}", "info") print_status(f"当前工作目录为 {working_dir}", "info")
load_config_from_file(config_path) 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": if args_dict["addr"] == "test":
print_status("使用测试环境地址", "info") print_status("使用测试环境地址", "info")
HTTPConfig.remote_addr = "https://uni-lab.test.bohrium.com/api/v1" 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 http_client
from unilabos.app.web import start_server from unilabos.app.web import start_server
from unilabos.app.register import register_devices_and_resources from unilabos.app.register import register_devices_and_resources
from unilabos.resources.graphio import modify_to_backend_format
from unilabos.ros.nodes.resource_tracker import ResourceTreeSet, ResourceDict
# 显示启动横幅 # 显示启动横幅
print_unilab_banner(args_dict) print_unilab_banner(args_dict)
@@ -356,7 +349,7 @@ def main():
if BasicConfig.upload_registry: if BasicConfig.upload_registry:
# 设备注册到服务端 - 需要 ak 和 sk # 设备注册到服务端 - 需要 ak 和 sk
if BasicConfig.ak and BasicConfig.sk: if args_dict.get("ak") and args_dict.get("sk"):
print_status("开始注册设备到服务端...", "info") print_status("开始注册设备到服务端...", "info")
try: try:
register_devices_and_resources(lab_registry) register_devices_and_resources(lab_registry)
@@ -375,23 +368,22 @@ def main():
args_dict["bridges"] = [] 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"]: if "fastapi" in args_dict["app_bridges"]:
args_dict["bridges"].append(http_client) args_dict["bridges"].append(http_client)
# 获取通信客户端仅支持WebSocket if "websocket" in args_dict["app_bridges"]:
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)
signal.signal(signal.SIGINT, _exit) def _exit(signum, frame):
signal.signal(signal.SIGTERM, _exit) comm_client.stop()
comm_client.start() sys.exit(0)
else:
print_status("SlaveMode跳过Websocket连接")
signal.signal(signal.SIGINT, _exit)
signal.signal(signal.SIGTERM, _exit)
comm_client.start()
args_dict["resources_mesh_config"] = {} args_dict["resources_mesh_config"] = {}
args_dict["resources_edge_config"] = resource_edge_info args_dict["resources_edge_config"] = resource_edge_info
# web visiualize 2D # web visiualize 2D

View File

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

View File

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

View File

@@ -421,7 +421,7 @@ class MessageProcessor:
ssl_context = ssl_module.create_default_context() ssl_context = ssl_module.create_default_context()
ws_logger = logging.getLogger("websockets.client") ws_logger = logging.getLogger("websockets.client")
# 日志级别已在 unilabos.utils.log 中统一配置为 WARNING ws_logger.setLevel(logging.INFO)
async with websockets.connect( async with websockets.connect(
self.websocket_url, self.websocket_url,
@@ -1197,7 +1197,7 @@ class WebSocketClient(BaseCommunicationClient):
}, },
} }
self.message_processor.send_message(message) self.message_processor.send_message(message)
logger.trace(f"[WebSocketClient] Device status published: {device_id}.{property_name}") logger.debug(f"[WebSocketClient] Device status published: {device_id}.{property_name}")
def publish_job_status( def publish_job_status(
self, feedback_data: dict, item: QueueItem, status: str, return_info: Optional[dict] = None self, feedback_data: dict, item: QueueItem, status: str, return_info: Optional[dict] = None

View File

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

View File

@@ -12,7 +12,6 @@ from serial import Serial
from serial.serialutil import SerialException from serial.serialutil import SerialException
from unilabos.messages import Point3D from unilabos.messages import Point3D
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
class GrblCNCConnectionError(Exception): class GrblCNCConnectionError(Exception):
@@ -33,7 +32,6 @@ class GrblCNCInfo:
class GrblCNCAsync: class GrblCNCAsync:
_status: str = "Offline" _status: str = "Offline"
_position: Point3D = Point3D(x=0.0, y=0.0, z=0.0) _position: Point3D = Point3D(x=0.0, y=0.0, z=0.0)
_ros_node: BaseROS2DeviceNode
def __init__(self, port: str, address: str = "1", limits: tuple[int, int, int, int, int, int] = (-150, 150, -200, 0, 0, 60)): def __init__(self, port: str, address: str = "1", limits: tuple[int, int, int, int, int, int] = (-150, 150, -200, 0, 0, 60)):
self.port = port self.port = port
@@ -60,9 +58,6 @@ class GrblCNCAsync:
self._run_future: Optional[Future[Any]] = None self._run_future: Optional[Future[Any]] = None
self._run_lock = Lock() self._run_lock = Lock()
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
def _read_all(self): def _read_all(self):
data = self._serial.read_until(b"\n") data = self._serial.read_until(b"\n")
data_decoded = data.decode() data_decoded = data.decode()
@@ -153,7 +148,7 @@ class GrblCNCAsync:
try: try:
await self._query(command) await self._query(command)
while True: while True:
await self._ros_node.sleep(0.2) # Wait for 0.5 seconds before polling again await asyncio.sleep(0.2) # Wait for 0.5 seconds before polling again
status = await self.get_status() status = await self.get_status()
if "Idle" in status: if "Idle" in status:
@@ -219,7 +214,7 @@ class GrblCNCAsync:
self._pose_number = i self._pose_number = i
self.pose_number_remaining = len(points) - i self.pose_number_remaining = len(points) - i
await self.set_position(point) await self.set_position(point)
await self._ros_node.sleep(0.5) await asyncio.sleep(0.5)
self._step_number = -1 self._step_number = -1
async def stop_operation(self): async def stop_operation(self):
@@ -240,7 +235,7 @@ class GrblCNCAsync:
async def open(self): async def open(self):
if self._read_task: if self._read_task:
raise GrblCNCConnectionError raise GrblCNCConnectionError
self._read_task = self._ros_node.create_task(self._read_loop()) self._read_task = asyncio.create_task(self._read_loop())
try: try:
await self.get_status() await self.get_status()

View File

@@ -2,8 +2,6 @@ import time
import asyncio import asyncio
from pydantic import BaseModel from pydantic import BaseModel
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
class Point3D(BaseModel): class Point3D(BaseModel):
x: float x: float
@@ -16,14 +14,9 @@ def d(a: Point3D, b: Point3D) -> float:
class MockCNCAsync: class MockCNCAsync:
_ros_node: BaseROS2DeviceNode["MockCNCAsync"]
def __init__(self): def __init__(self):
self._position: Point3D = Point3D(x=0.0, y=0.0, z=0.0) self._position: Point3D = Point3D(x=0.0, y=0.0, z=0.0)
self._status = "Idle" self._status = "Idle"
def post_create(self, ros_node):
self._ros_node = ros_node
@property @property
def position(self) -> Point3D: def position(self) -> Point3D:
@@ -45,5 +38,5 @@ class MockCNCAsync:
self._position.x = current_pos.x + (position.x - current_pos.x) / 20 * (i+1) self._position.x = current_pos.x + (position.x - current_pos.x) / 20 * (i+1)
self._position.y = current_pos.y + (position.y - current_pos.y) / 20 * (i+1) self._position.y = current_pos.y + (position.y - current_pos.y) / 20 * (i+1)
self._position.z = current_pos.z + (position.z - current_pos.z) / 20 * (i+1) self._position.z = current_pos.z + (position.z - current_pos.z) / 20 * (i+1)
await self._ros_node.sleep(move_time / 20) await asyncio.sleep(move_time / 20)
self._status = "Idle" self._status = "Idle"

View File

@@ -1,296 +0,0 @@
# -*- coding: utf-8 -*-
import serial
import time
import csv
import threading
import os
from collections import deque
from typing import Dict, Any, Optional
from pylabrobot.resources import Deck
from unilabos.devices.workstation.workstation_base import WorkstationBase
class ElectrolysisWaterPlatform(WorkstationBase):
"""
电解水平台工作站
基于 WorkstationBase 的电解水实验平台,支持串口通信和数据采集
"""
def __init__(
self,
deck: Deck,
port: str = "COM10",
baudrate: int = 115200,
csv_path: Optional[str] = None,
timeout: float = 0.2,
**kwargs
):
super().__init__(deck, **kwargs)
# ========== 配置 ==========
self.port = port
self.baudrate = baudrate
# 如果没有指定路径,默认保存在代码文件所在目录
if csv_path is None:
current_dir = os.path.dirname(os.path.abspath(__file__))
self.csv_path = os.path.join(current_dir, "stm32_data.csv")
else:
self.csv_path = csv_path
self.ser_timeout = timeout
self.chunk_read = 128
# 串口对象
self.ser: Optional[serial.Serial] = None
self.stop_flag = False
# 线程对象
self.rx_thread: Optional[threading.Thread] = None
self.tx_thread: Optional[threading.Thread] = None
# ==== 接收(下位机->上位机):固定 1+13+1 = 15 字节 ====
self.RX_HEAD = 0x3E
self.RX_TAIL = 0x3E
self.RX_FRAME_LEN = 1 + 13 + 1 # 15
# ==== 发送(上位机->下位机):固定 1+9+1 = 11 字节 ====
self.TX_HEAD = 0x3E
self.TX_TAIL = 0xE3 # 协议图中标注 E3 作为帧尾
self.TX_FRAME_LEN = 1 + 9 + 1 # 11
def open_serial(self, port: Optional[str] = None, baudrate: Optional[int] = None, timeout: Optional[float] = None) -> Optional[serial.Serial]:
"""打开串口"""
port = port or self.port
baudrate = baudrate or self.baudrate
timeout = timeout or self.ser_timeout
try:
ser = serial.Serial(port, baudrate, timeout=timeout)
print(f"[OK] 串口 {port} 已打开,波特率 {baudrate}")
ser.reset_input_buffer()
ser.reset_output_buffer()
self.ser = ser
return ser
except serial.SerialException as e:
print(f"[ERR] 无法打开串口 {port}: {e}")
return None
def close_serial(self):
"""关闭串口"""
if self.ser and self.ser.is_open:
self.ser.close()
print("[INFO] 串口已关闭")
@staticmethod
def u16_be(h: int, l: int) -> int:
"""将两个字节组合成16位无符号整数大端序"""
return ((h & 0xFF) << 8) | (l & 0xFF)
@staticmethod
def split_u16_be(val: int) -> tuple:
"""返回 (高字节, 低字节),输入会夹到 0..65535"""
v = int(max(0, min(65535, int(val))))
return (v >> 8) & 0xFF, v & 0xFF
# ================== 接收固定15字节 ==================
def parse_rx_payload(self, dat13: bytes) -> Optional[Dict[str, Any]]:
"""解析 13 字节数据区(下位机发送到上位机)"""
if len(dat13) != 13:
return None
current_mA = self.u16_be(dat13[0], dat13[1])
voltage_mV = self.u16_be(dat13[2], dat13[3])
temperature_raw = self.u16_be(dat13[4], dat13[5])
tds_ppm = self.u16_be(dat13[6], dat13[7])
gas_sccm = self.u16_be(dat13[8], dat13[9])
liquid_mL = self.u16_be(dat13[10], dat13[11])
ph_raw = dat13[12] & 0xFF
return {
"Current_mA": current_mA,
"Voltage_mV": voltage_mV,
"Temperature_C": round(temperature_raw / 100.0, 2),
"TDS_ppm": tds_ppm,
"GasFlow_sccm": gas_sccm,
"LiquidFlow_mL": liquid_mL,
"pH": round(ph_raw / 10.0, 2)
}
def try_parse_rx_frame(self, frame15: bytes) -> Optional[Dict[str, Any]]:
"""尝试解析接收帧"""
if len(frame15) != self.RX_FRAME_LEN:
return None
if frame15[0] != self.RX_HEAD or frame15[-1] != self.RX_TAIL:
return None
return self.parse_rx_payload(frame15[1:-1])
def rx_thread_fn(self):
"""接收线程函数"""
headers = ["Timestamp", "Current_mA", "Voltage_mV",
"Temperature_C", "TDS_ppm", "GasFlow_sccm", "LiquidFlow_mL", "pH"]
new_file = not os.path.exists(self.csv_path)
f = open(self.csv_path, mode='a', newline='', encoding='utf-8')
writer = csv.writer(f)
if new_file:
writer.writerow(headers)
f.flush()
buf = deque(maxlen=8192)
print(f"[RX] 开始接收(帧长 {self.RX_FRAME_LEN} 字节);写入:{self.csv_path}")
try:
while not self.stop_flag and self.ser and self.ser.is_open:
chunk = self.ser.read(self.chunk_read)
if chunk:
buf.extend(chunk)
while True:
# 找帧头
try:
start = next(i for i, b in enumerate(buf) if b == self.RX_HEAD)
except StopIteration:
buf.clear()
break
if start > 0:
for _ in range(start):
buf.popleft()
if len(buf) < self.RX_FRAME_LEN:
break
candidate = bytes([buf[i] for i in range(self.RX_FRAME_LEN)])
if candidate[-1] == self.RX_TAIL:
parsed = self.try_parse_rx_frame(candidate)
for _ in range(self.RX_FRAME_LEN):
buf.popleft()
if parsed:
ts = time.strftime("%Y-%m-%d %H:%M:%S")
row = [ts,
parsed["Current_mA"], parsed["Voltage_mV"],
parsed["Temperature_C"], parsed["TDS_ppm"],
parsed["GasFlow_sccm"], parsed["LiquidFlow_mL"],
parsed["pH"]]
writer.writerow(row)
f.flush()
# 若不想打印可注释下一行
# print(f"[{ts}] I={parsed['Current_mA']} mA, V={parsed['Voltage_mV']} mV, "
# f"T={parsed['Temperature_C']} °C, TDS={parsed['TDS_ppm']}, "
# f"Gas={parsed['GasFlow_sccm']} sccm, Liq={parsed['LiquidFlow_mL']} mL, pH={parsed['pH']}")
else:
# 头不变尾不对丢1字节继续对齐
buf.popleft()
else:
time.sleep(0.01)
finally:
f.close()
print("[RX] 接收线程退出CSV 已关闭")
# ================== 发送固定11字节 ==================
def build_tx_frame(self, mode: int, current_ma: int, voltage_mv: int, temp_c: float, ki: float, pump_percent: float) -> bytes:
"""
发送帧HEAD + [mode, I_hi, I_lo, V_hi, V_lo, T_hi, T_lo, Ki_byte, Pump_byte] + TAIL
- mode: 0=恒压, 1=恒流
- current_ma: mA (0..65535)
- voltage_mv: mV (0..65535)
- temp_c: ℃,将 *100 后拆分为高/低字节
- ki: 0.0..20.0 -> byte = round(ki * 10) 夹到 0..200
- pump_percent: 0..100 -> byte = round(pump * 2) 夹到 0..200
"""
mode_b = 1 if int(mode) == 1 else 0
i_hi, i_lo = self.split_u16_be(current_ma)
v_hi, v_lo = self.split_u16_be(voltage_mv)
t100 = int(round(float(temp_c) * 100.0))
t_hi, t_lo = self.split_u16_be(t100)
ki_b = int(max(0, min(200, round(float(ki) * 10))))
pump_b = int(max(0, min(200, round(float(pump_percent) * 2))))
return bytes((
self.TX_HEAD,
mode_b,
i_hi, i_lo,
v_hi, v_lo,
t_hi, t_lo,
ki_b,
pump_b,
self.TX_TAIL
))
def tx_thread_fn(self):
"""
发送线程函数
用户输入 6 个用逗号分隔的数值:
mode,current_mA,voltage_mV,set_temp_C,Ki,pump_percent
例如: 0,1000,500,0,0,50
"""
print("\n输入 6 个值(用英文逗号分隔),顺序为:")
print("mode,current_mA,voltage_mV,set_temp_C,Ki,pump_percent")
print("示例恒压0,500,1000,25,0,100 stop 结束)\n")
print("示例恒流1,1000,500,25,0,100 stop 结束)\n")
print("示例恒流1,2000,500,25,0,100 stop 结束)\n")
# 1,2000,500,25,0,100
while not self.stop_flag and self.ser and self.ser.is_open:
try:
line = input(">>> ").strip()
except EOFError:
self.stop_flag = True
break
if not line:
continue
if line.lower() == "stop":
self.stop_flag = True
print("[SYS] 停止程序")
break
try:
parts = [p.strip() for p in line.split(",")]
if len(parts) != 6:
raise ValueError("需要 6 个逗号分隔的数值")
mode = int(parts[0])
i_ma = int(float(parts[1]))
v_mv = int(float(parts[2]))
t_c = float(parts[3])
ki = float(parts[4])
pump = float(parts[5])
frame = self.build_tx_frame(mode, i_ma, v_mv, t_c, ki, pump)
self.ser.write(frame)
print("[TX]", " ".join(f"{b:02X}" for b in frame))
except Exception as e:
print("[TX] 输入/打包失败:", e)
print("格式mode,current_mA,voltage_mV,set_temp_C,Ki,pump_percent")
continue
def start(self):
"""启动电解水平台"""
self.ser = self.open_serial()
if self.ser:
try:
self.rx_thread = threading.Thread(target=self.rx_thread_fn, daemon=True)
self.tx_thread = threading.Thread(target=self.tx_thread_fn, daemon=True)
self.rx_thread.start()
self.tx_thread.start()
print("[INFO] 电解水平台已启动")
self.tx_thread.join() # 等待用户输入线程结束(输入 stop
finally:
self.close_serial()
def stop(self):
"""停止电解水平台"""
self.stop_flag = True
if self.rx_thread and self.rx_thread.is_alive():
self.rx_thread.join(timeout=2.0)
if self.tx_thread and self.tx_thread.is_alive():
self.tx_thread.join(timeout=2.0)
self.close_serial()
print("[INFO] 电解水平台已停止")
# ================== 主入口 ==================
if __name__ == "__main__":
# 创建一个简单的 Deck 用于测试
from pylabrobot.resources import Deck
deck = Deck()
platform = ElectrolysisWaterPlatform(deck)
platform.start()

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,11 @@
from __future__ import annotations from __future__ import annotations
import re
import traceback
from typing import List, Sequence, Optional, Literal, Union, Iterator, Dict, Any, Callable, Set, cast
from collections import Counter
import asyncio import asyncio
import time import time
import traceback import pprint as pp
from collections import Counter
from typing import List, Sequence, Optional, Literal, Union, Iterator, Dict, Any, Callable, Set, cast
from pylabrobot.liquid_handling import LiquidHandler, LiquidHandlerBackend, LiquidHandlerChatterboxBackend, Strictness from pylabrobot.liquid_handling import LiquidHandler, LiquidHandlerBackend, LiquidHandlerChatterboxBackend, Strictness
from pylabrobot.liquid_handling.liquid_handler import TipPresenceProbingMethod from pylabrobot.liquid_handling.liquid_handler import TipPresenceProbingMethod
from pylabrobot.liquid_handling.standard import GripDirection from pylabrobot.liquid_handling.standard import GripDirection
@@ -25,8 +25,6 @@ from pylabrobot.resources import (
Tip, Tip,
) )
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
class LiquidHandlerMiddleware(LiquidHandler): class LiquidHandlerMiddleware(LiquidHandler):
def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool = False, channel_num: int = 8): def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool = False, channel_num: int = 8):
@@ -538,7 +536,6 @@ class LiquidHandlerMiddleware(LiquidHandler):
class LiquidHandlerAbstract(LiquidHandlerMiddleware): class LiquidHandlerAbstract(LiquidHandlerMiddleware):
"""Extended LiquidHandler with additional operations.""" """Extended LiquidHandler with additional operations."""
support_touch_tip = True support_touch_tip = True
_ros_node: BaseROS2DeviceNode
def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool=False, channel_num:int = 8): def __init__(self, backend: LiquidHandlerBackend, deck: Deck, simulator: bool=False, channel_num:int = 8):
"""Initialize a LiquidHandler. """Initialize a LiquidHandler.
@@ -551,11 +548,8 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
self.group_info = dict() self.group_info = dict()
super().__init__(backend, deck, simulator, channel_num) super().__init__(backend, deck, simulator, channel_num)
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
@classmethod @classmethod
def set_liquid(cls, wells: list[Well], liquid_names: list[str], volumes: list[float]): def set_liquid(self, wells: list[Well], liquid_names: list[str], volumes: list[float]):
"""Set the liquid in a well.""" """Set the liquid in a well."""
for well, liquid_name, volume in zip(wells, liquid_names, volumes): for well, liquid_name, volume in zip(wells, liquid_names, volumes):
well.set_liquids([(liquid_name, volume)]) # type: ignore well.set_liquids([(liquid_name, volume)]) # type: ignore
@@ -1087,7 +1081,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
print(f"Waiting time: {msg}") print(f"Waiting time: {msg}")
print(f"Current time: {time.strftime('%H:%M:%S')}") print(f"Current time: {time.strftime('%H:%M:%S')}")
print(f"Time to finish: {time.strftime('%H:%M:%S', time.localtime(time.time() + seconds))}") print(f"Time to finish: {time.strftime('%H:%M:%S', time.localtime(time.time() + seconds))}")
await self._ros_node.sleep(seconds) await asyncio.sleep(seconds)
if msg: if msg:
print(f"Done: {msg}") print(f"Done: {msg}")
print(f"Current time: {time.strftime('%H:%M:%S')}") print(f"Current time: {time.strftime('%H:%M:%S')}")

View File

@@ -30,7 +30,6 @@ from pylabrobot.liquid_handling.standard import (
from pylabrobot.resources import Tip, Deck, Plate, Well, TipRack, Resource, Container, Coordinate, TipSpot, Trash from pylabrobot.resources import Tip, Deck, Plate, Well, TipRack, Resource, Container, Coordinate, TipSpot, Trash
from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
class PRCXIError(RuntimeError): class PRCXIError(RuntimeError):
@@ -163,10 +162,6 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
) )
super().__init__(backend=self._unilabos_backend, deck=deck, simulator=simulator, channel_num=channel_num) super().__init__(backend=self._unilabos_backend, deck=deck, simulator=simulator, channel_num=channel_num)
def post_init(self, ros_node: BaseROS2DeviceNode):
super().post_init(ros_node)
self._unilabos_backend.post_init(ros_node)
def set_liquid(self, wells: list[Well], liquid_names: list[str], volumes: list[float]): def set_liquid(self, wells: list[Well], liquid_names: list[str], volumes: list[float]):
return super().set_liquid(wells, liquid_names, volumes) return super().set_liquid(wells, liquid_names, volumes)
@@ -429,7 +424,6 @@ class PRCXI9300Backend(LiquidHandlerBackend):
_num_channels = 8 # 默认通道数为 8 _num_channels = 8 # 默认通道数为 8
_is_reset_ok = False _is_reset_ok = False
_ros_node: BaseROS2DeviceNode
@property @property
def is_reset_ok(self) -> bool: def is_reset_ok(self) -> bool:
@@ -462,9 +456,6 @@ class PRCXI9300Backend(LiquidHandlerBackend):
self._execute_setup = setup self._execute_setup = setup
self.debug = debug self.debug = debug
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
def create_protocol(self, protocol_name): def create_protocol(self, protocol_name):
self.protocol_name = protocol_name self.protocol_name = protocol_name
self.steps_todo_list = [] self.steps_todo_list = []
@@ -509,7 +500,7 @@ class PRCXI9300Backend(LiquidHandlerBackend):
self.api_client.call("IAutomation", "Reset") self.api_client.call("IAutomation", "Reset")
while not self.is_reset_ok: while not self.is_reset_ok:
print("Waiting for PRCXI9300 to reset...") print("Waiting for PRCXI9300 to reset...")
await self._ros_node.sleep(1) await asyncio.sleep(1)
print("PRCXI9300 reset successfully.") print("PRCXI9300 reset successfully.")
except ConnectionRefusedError as e: except ConnectionRefusedError as e:
raise RuntimeError( raise RuntimeError(
@@ -542,9 +533,7 @@ class PRCXI9300Backend(LiquidHandlerBackend):
tipspot_index = tipspot.parent.children.index(tipspot) tipspot_index = tipspot.parent.children.index(tipspot)
tip_columns.append(tipspot_index // 8) tip_columns.append(tipspot_index // 8)
if len(set(tip_columns)) != 1: if len(set(tip_columns)) != 1:
raise ValueError( raise ValueError("All pickups must be from the same tip column. Found different columns: " + str(tip_columns))
"All pickups must be from the same tip column. Found different columns: " + str(tip_columns)
)
PlateNo = plate_indexes[0] + 1 PlateNo = plate_indexes[0] + 1
hole_col = tip_columns[0] + 1 hole_col = tip_columns[0] + 1
hole_row = 1 hole_row = 1
@@ -1120,15 +1109,12 @@ class PRCXI9300Api:
"LiquidDispensingMethod": liquid_method, "LiquidDispensingMethod": liquid_method,
} }
class DefaultLayout: class DefaultLayout:
def __init__(self, product_name: str = "PRCXI9300"): def __init__(self, product_name: str = "PRCXI9300"):
self.labresource = {} self.labresource = {}
if product_name not in ["PRCXI9300", "PRCXI9320"]: if product_name not in ["PRCXI9300", "PRCXI9320"]:
raise ValueError( raise ValueError(f"Unsupported product_name: {product_name}. Only 'PRCXI9300' and 'PRCXI9320' are supported.")
f"Unsupported product_name: {product_name}. Only 'PRCXI9300' and 'PRCXI9320' are supported."
)
if product_name == "PRCXI9300": if product_name == "PRCXI9300":
self.rows = 2 self.rows = 2
@@ -1143,93 +1129,25 @@ class DefaultLayout:
self.layout = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16] self.layout = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
self.trash_slot = 16 self.trash_slot = 16
self.waste_liquid_slot = 12 self.waste_liquid_slot = 12
self.default_layout = { self.default_layout = {"MatrixId":f"{time.time()}","MatrixName":f"{time.time()}","MatrixCount":16,"WorkTablets":
"MatrixId": f"{time.time()}", [{"Number": 1, "Code": "T1", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
"MatrixName": f"{time.time()}", {"Number": 2, "Code": "T2", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
"MatrixCount": 16, {"Number": 3, "Code": "T3", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
"WorkTablets": [ {"Number": 4, "Code": "T4", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{ {"Number": 5, "Code": "T5", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
"Number": 1, {"Number": 6, "Code": "T6", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
"Code": "T1", {"Number": 7, "Code": "T7", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}, {"Number": 8, "Code": "T8", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
}, {"Number": 9, "Code": "T9", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{ {"Number": 10, "Code": "T10", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
"Number": 2, {"Number": 11, "Code": "T11", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
"Code": "T2", {"Number": 12, "Code": "T12", "Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0}}, # 这个设置成废液槽,用储液槽表示
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}, {"Number": 13, "Code": "T13", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
}, {"Number": 14, "Code": "T14", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
{ {"Number": 15, "Code": "T15", "Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}},
"Number": 3, {"Number": 16, "Code": "T16", "Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0}} # 这个设置成垃圾桶,用储液槽表示
"Code": "T3", ]
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0}, }
},
{
"Number": 4,
"Code": "T4",
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
},
{
"Number": 5,
"Code": "T5",
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
},
{
"Number": 6,
"Code": "T6",
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
},
{
"Number": 7,
"Code": "T7",
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
},
{
"Number": 8,
"Code": "T8",
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
},
{
"Number": 9,
"Code": "T9",
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
},
{
"Number": 10,
"Code": "T10",
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
},
{
"Number": 11,
"Code": "T11",
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
},
{
"Number": 12,
"Code": "T12",
"Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0},
}, # 这个设置成废液槽,用储液槽表示
{
"Number": 13,
"Code": "T13",
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
},
{
"Number": 14,
"Code": "T14",
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
},
{
"Number": 15,
"Code": "T15",
"Material": {"uuid": "57b1e4711e9e4a32b529f3132fc5931f", "materialEnum": 0},
},
{
"Number": 16,
"Code": "T16",
"Material": {"uuid": "730067cf07ae43849ddf4034299030e9", "materialEnum": 0},
}, # 这个设置成垃圾桶,用储液槽表示
],
}
def get_layout(self) -> Dict[str, Any]: def get_layout(self) -> Dict[str, Any]:
return { return {
@@ -1237,7 +1155,7 @@ class DefaultLayout:
"columns": self.columns, "columns": self.columns,
"layout": self.layout, "layout": self.layout,
"trash_slot": self.trash_slot, "trash_slot": self.trash_slot,
"waste_liquid_slot": self.waste_liquid_slot, "waste_liquid_slot": self.waste_liquid_slot
} }
def get_trash_slot(self) -> int: def get_trash_slot(self) -> int:
@@ -1260,19 +1178,17 @@ class DefaultLayout:
reserved_positions = {12, 16} reserved_positions = {12, 16}
available_positions = [i for i in range(1, 17) if i not in reserved_positions] available_positions = [i for i in range(1, 17) if i not in reserved_positions]
# 计算总需求 # 计算总需求
total_needed = sum(count for _, _, count in needs) total_needed = sum(count for _, _, count in needs)
if total_needed > len(available_positions): if total_needed > len(available_positions):
raise ValueError( raise ValueError(f"需要 {total_needed} 个位置,但只有 {len(available_positions)} 个可用位置排除位置12和16")
f"需要 {total_needed} 个位置,但只有 {len(available_positions)} 个可用位置排除位置12和16"
)
# 依次分配位置 # 依次分配位置
current_pos = 0 current_pos = 0
for reagent_name, material_name, count in needs: for reagent_name, material_name, count in needs:
material_uuid = self.labresource[material_name]["uuid"] material_uuid = self.labresource[material_name]['uuid']
material_enum = self.labresource[material_name]["materialEnum"] material_enum = self.labresource[material_name]['materialEnum']
for _ in range(count): for _ in range(count):
if current_pos >= len(available_positions): if current_pos >= len(available_positions):
@@ -1280,18 +1196,17 @@ class DefaultLayout:
position = available_positions[current_pos] position = available_positions[current_pos]
# 找到对应的tablet并更新 # 找到对应的tablet并更新
for tablet in self.default_layout["WorkTablets"]: for tablet in self.default_layout['WorkTablets']:
if tablet["Number"] == position: if tablet['Number'] == position:
tablet["Material"]["uuid"] = material_uuid tablet['Material']['uuid'] = material_uuid
tablet["Material"]["materialEnum"] = material_enum tablet['Material']['materialEnum'] = material_enum
layout_list.append( layout_list.append(dict(reagent_name=reagent_name, material_name=material_name, positions=position))
dict(reagent_name=reagent_name, material_name=material_name, positions=position)
)
break break
current_pos += 1 current_pos += 1
return self.default_layout, layout_list return self.default_layout, layout_list
if __name__ == "__main__": if __name__ == "__main__":
# Example usage # Example usage
# 1. 用导出的json给每个T1 T2板子设定相应的物料如果是孔板和枪头盒要对应区分 # 1. 用导出的json给每个T1 T2板子设定相应的物料如果是孔板和枪头盒要对应区分
@@ -1387,7 +1302,10 @@ if __name__ == "__main__":
# # # plate2.set_well_liquids(plate_2_liquids) # # # plate2.set_well_liquids(plate_2_liquids)
# handler = PRCXI9300Handler(deck=deck, host="10.181.214.132", port=9999,
# handler = PRCXI9300Handler(deck=deck, host="10.181.214.132", port=9999,
# timeout=10.0, setup=False, debug=False, # timeout=10.0, setup=False, debug=False,
# simulator=True, # simulator=True,
# matrix_id="71593", # matrix_id="71593",
@@ -1473,7 +1391,10 @@ if __name__ == "__main__":
# # input("Press Enter to continue...") # Wait for user input before proceeding # # input("Press Enter to continue...") # Wait for user input before proceeding
# # print("PRCXI9300Handler initialized with deck and host settings.") # # print("PRCXI9300Handler initialized with deck and host settings.")
### 9320 ###
### 9320 ###
deck = PRCXI9300Deck(name="PRCXI_Deck", size_x=100, size_y=100, size_z=100) deck = PRCXI9300Deck(name="PRCXI_Deck", size_x=100, size_y=100, size_z=100)
@@ -1491,15 +1412,12 @@ if __name__ == "__main__":
new_plate: PRCXI9300Container = PRCXI9300Container.deserialize(well_containers) new_plate: PRCXI9300Container = PRCXI9300Container.deserialize(well_containers)
return new_plate return new_plate
def get_tip_rack(name: str, child_prefix: str = "tip") -> PRCXI9300Container: def get_tip_rack(name: str, child_prefix: str="tip") -> PRCXI9300Container:
tip_racks = opentrons_96_tiprack_10ul(name).serialize() tip_racks = opentrons_96_tiprack_10ul(name).serialize()
tip_rack = PRCXI9300Container( tip_rack = PRCXI9300Container(
name=name, name=name, size_x=50, size_y=50, size_z=10, category="tip_rack", ordering=collections.OrderedDict({
size_x=50, k: f"{child_prefix}_{k}" for k, v in tip_racks["ordering"].items()
size_y=50, })
size_z=10,
category="tip_rack",
ordering=collections.OrderedDict({k: f"{child_prefix}_{k}" for k, v in tip_racks["ordering"].items()}),
) )
tip_rack_serialized = tip_rack.serialize() tip_rack_serialized = tip_rack.serialize()
tip_rack_serialized["parent_name"] = deck.name tip_rack_serialized["parent_name"] = deck.name
@@ -1711,7 +1629,6 @@ if __name__ == "__main__":
) )
backend: PRCXI9300Backend = handler.backend backend: PRCXI9300Backend = handler.backend
from pylabrobot.resources import set_volume_tracking from pylabrobot.resources import set_volume_tracking
set_volume_tracking(enabled=True) set_volume_tracking(enabled=True)
# res = backend.api_client.get_all_materials() # res = backend.api_client.get_all_materials()
asyncio.run(handler.setup()) # Initialize the handler and setup the connection asyncio.run(handler.setup()) # Initialize the handler and setup the connection
@@ -1723,10 +1640,10 @@ if __name__ == "__main__":
for well in plate13.get_all_items(): for well in plate13.get_all_items():
# well_pos = well.name.split("_")[1] # 走一行 # well_pos = well.name.split("_")[1] # 走一行
# if well_pos.startswith("A"): # if well_pos.startswith("A"):
if well.name.startswith("PlateT13"): # 走整个Plate if well.name.startswith("PlateT13"): # 走整个Plate
asyncio.run(handler.dispense([well], [0.01], [0])) asyncio.run(handler.dispense([well], [0.01], [0]))
# asyncio.run(handler.dispense([plate10.get_item("H12")], [1], [0])) # asyncio.run(handler.dispense([plate10.get_item("H12")], [1], [0]))
# asyncio.run(handler.dispense([plate13.get_item("A1")], [1], [0])) # asyncio.run(handler.dispense([plate13.get_item("A1")], [1], [0]))
# asyncio.run(handler.dispense([plate14.get_item("C5")], [1], [0])) # asyncio.run(handler.dispense([plate14.get_item("C5")], [1], [0]))
@@ -1735,25 +1652,26 @@ if __name__ == "__main__":
asyncio.run(handler.run_protocol()) asyncio.run(handler.run_protocol())
time.sleep(5) time.sleep(5)
os._exit(0) os._exit(0)
# 第一种情景:一个孔往多个孔加液 # 第一种情景:一个孔往多个孔加液
# plate_2_liquids = handler.set_group("water", [plate2.children[0]], [300]) # plate_2_liquids = handler.set_group("water", [plate2.children[0]], [300])
# plate5_liquids = handler.set_group("master_mix", plate5.children[:23], [100]*23) # plate5_liquids = handler.set_group("master_mix", plate5.children[:23], [100]*23)
# 第二个情景:多个孔往多个孔加液(但是个数得对应) # 第二个情景:多个孔往多个孔加液(但是个数得对应)
plate_2_liquids = handler.set_group("water", plate2.children[:23], [300] * 23) plate_2_liquids = handler.set_group("water", plate2.children[:23], [300]*23)
plate5_liquids = handler.set_group("master_mix", plate5.children[:23], [100] * 23) plate5_liquids = handler.set_group("master_mix", plate5.children[:23], [100]*23)
# plate11.set_well_liquids([("Water", 100) if (i % 8 == 0 and i // 8 < 6) else (None, 100) for i in range(96)]) # Set liquids for every 8 wells in plate8 # plate11.set_well_liquids([("Water", 100) if (i % 8 == 0 and i // 8 < 6) else (None, 100) for i in range(96)]) # Set liquids for every 8 wells in plate8
# plate11.set_well_liquids([("Water", 100) if (i % 8 == 0 and i // 8 < 6) else (None, 100) for i in range(96)]) # Set liquids for every 8 wells in plate8 # plate11.set_well_liquids([("Water", 100) if (i % 8 == 0 and i // 8 < 6) else (None, 100) for i in range(96)]) # Set liquids for every 8 wells in plate8
# A = tree_to_list([resource_plr_to_ulab(deck)]) # A = tree_to_list([resource_plr_to_ulab(deck)])
# # with open("deck.json", "w", encoding="utf-8") as f: # # with open("deck.json", "w", encoding="utf-8") as f:
# # json.dump(A, f, indent=4, ensure_ascii=False) # # json.dump(A, f, indent=4, ensure_ascii=False)
# print(plate11.get_well(0).tracker.get_used_volume()) # print(plate11.get_well(0).tracker.get_used_volume())
# Initialize the backend and setup the connection # Initialize the backend and setup the connection
asyncio.run(handler.transfer_group("water", "master_mix", 10)) # Reset tip tracking asyncio.run(handler.transfer_group("water", "master_mix", 10)) # Reset tip tracking
# asyncio.run(handler.pick_up_tips([plate8.children[8]],[0])) # asyncio.run(handler.pick_up_tips([plate8.children[8]],[0]))
# print(plate8.children[8]) # print(plate8.children[8])
# asyncio.run(handler.run_protocol()) # asyncio.run(handler.run_protocol())
@@ -1767,118 +1685,121 @@ if __name__ == "__main__":
# print(plate1.children[0]) # print(plate1.children[0])
# asyncio.run(handler.discard_tips([0])) # asyncio.run(handler.discard_tips([0]))
# asyncio.run(handler.add_liquid( # asyncio.run(handler.add_liquid(
# asp_vols=[10]*7, # asp_vols=[10]*7,
# dis_vols=[10]*7, # dis_vols=[10]*7,
# reagent_sources=plate11.children[:7], # reagent_sources=plate11.children[:7],
# targets=plate1.children[2:9], # targets=plate1.children[2:9],
# use_channels=[0], # use_channels=[0],
# flow_rates=[None] * 7, # flow_rates=[None] * 7,
# offsets=[Coordinate(0, 0, 0)] * 7, # offsets=[Coordinate(0, 0, 0)] * 7,
# liquid_height=[None] * 7, # liquid_height=[None] * 7,
# blow_out_air_volume=[None] * 2, # blow_out_air_volume=[None] * 2,
# delays=None, # delays=None,
# mix_time=3, # mix_time=3,
# mix_vol=5, # mix_vol=5,
# spread="custom", # spread="custom",
# )) # ))
# asyncio.run(handler.run_protocol()) # Run the protocol # asyncio.run(handler.run_protocol()) # Run the protocol
# # # asyncio.run(handler.transfer_liquid(
# # # asp_vols=[10]*2,
# # # dis_vols=[10]*2,
# # # sources=plate11.children[:2],
# # # targets=plate11.children[-2:],
# # # use_channels=[0],
# # # offsets=[Coordinate(0, 0, 0)] * 4,
# # # liquid_height=[None] * 2,
# # # blow_out_air_volume=[None] * 2,
# # # delays=None,
# # # mix_times=3,
# # # mix_vol=5,
# # # spread="wide",
# # # tip_racks=[plate8]
# # # ))
# # # asyncio.run(handler.remove_liquid(
# # # vols=[10]*2,
# # # sources=plate11.children[:2],
# # # waste_liquid=plate11.children[43],
# # # use_channels=[0],
# # # offsets=[Coordinate(0, 0, 0)] * 4,
# # # liquid_height=[None] * 2,
# # # blow_out_air_volume=[None] * 2,
# # # delays=None,
# # # spread="wide"
# # # ))
# # asyncio.run(handler.run_protocol())
# # # asyncio.run(handler.discard_tips())
# # # asyncio.run(handler.mix(well_containers.children[:8
# # # ], mix_time=3, mix_vol=50, height_to_bottom=0.5, offsets=Coordinate(0, 0, 0), mix_rate=100))
# # #print(json.dumps(handler._unilabos_backend.steps_todo_list, indent=2)) # Print matrix info
# # # asyncio.run(handler.remove_liquid( # # # asyncio.run(handler.transfer_liquid(
# # # vols=[100]*16, # # # asp_vols=[10]*2,
# # # sources=well_containers.children[-16:], # # # dis_vols=[10]*2,
# # # waste_liquid=well_containers.children[:16], # 这个有些奇怪,但是好像也只能这么写 # # # sources=plate11.children[:2],
# # # use_channels=[0, 1, 2, 3, 4, 5, 6, 7], # # # targets=plate11.children[-2:],
# # # flow_rates=[None] * 32, # # # use_channels=[0],
# # # offsets=[Coordinate(0, 0, 0)] * 32, # # # offsets=[Coordinate(0, 0, 0)] * 4,
# # # liquid_height=[None] * 32, # # # liquid_height=[None] * 2,
# # # blow_out_air_volume=[None] * 32, # # # blow_out_air_volume=[None] * 2,
# # # spread="wide", # # # delays=None,
# # # )) # # # mix_times=3,
# # # asyncio.run(handler.transfer_liquid( # # # mix_vol=5,
# # # asp_vols=[100]*16, # # # spread="wide",
# # # dis_vols=[100]*16, # # # tip_racks=[plate8]
# # # tip_racks=[tip_rack], # # # ))
# # # sources=well_containers.children[-16:],
# # # targets=well_containers.children[:16], # # # asyncio.run(handler.remove_liquid(
# # # use_channels=[0, 1, 2, 3, 4, 5, 6, 7], # # # vols=[10]*2,
# # # offsets=[Coordinate(0, 0, 0)] * 32, # # # sources=plate11.children[:2],
# # # asp_flow_rates=[None] * 16, # # # waste_liquid=plate11.children[43],
# # # dis_flow_rates=[None] * 16, # # # use_channels=[0],
# # # liquid_height=[None] * 32, # # # offsets=[Coordinate(0, 0, 0)] * 4,
# # # blow_out_air_volume=[None] * 32, # # # liquid_height=[None] * 2,
# # # mix_times=3, # # # blow_out_air_volume=[None] * 2,
# # # mix_vol=50, # # # delays=None,
# # # spread="wide", # # # spread="wide"
# # # )) # # # ))
# # print(json.dumps(handler._unilabos_backend.steps_todo_list, indent=2)) # Print matrix info # # asyncio.run(handler.run_protocol())
# # # input("pick_up_tips add step")
# asyncio.run(handler.run_protocol()) # Run the protocol # # # asyncio.run(handler.discard_tips())
# # # input("Running protocol...") # # # asyncio.run(handler.mix(well_containers.children[:8
# # # input("Press Enter to continue...") # Wait for user input before proceeding # # # ], mix_time=3, mix_vol=50, height_to_bottom=0.5, offsets=Coordinate(0, 0, 0), mix_rate=100))
# # # print("PRCXI9300Handler initialized with deck and host settings.") # # #print(json.dumps(handler._unilabos_backend.steps_todo_list, indent=2)) # Print matrix info
# # # asyncio.run(handler.remove_liquid(
# # # vols=[100]*16,
# # # sources=well_containers.children[-16:],
# # # waste_liquid=well_containers.children[:16], # 这个有些奇怪,但是好像也只能这么写
# # # use_channels=[0, 1, 2, 3, 4, 5, 6, 7],
# # # flow_rates=[None] * 32,
# # # offsets=[Coordinate(0, 0, 0)] * 32,
# # # liquid_height=[None] * 32,
# # # blow_out_air_volume=[None] * 32,
# # # spread="wide",
# # # ))
# # # asyncio.run(handler.transfer_liquid(
# # # asp_vols=[100]*16,
# # # dis_vols=[100]*16,
# # # tip_racks=[tip_rack],
# # # sources=well_containers.children[-16:],
# # # targets=well_containers.children[:16],
# # # use_channels=[0, 1, 2, 3, 4, 5, 6, 7],
# # # offsets=[Coordinate(0, 0, 0)] * 32,
# # # asp_flow_rates=[None] * 16,
# # # dis_flow_rates=[None] * 16,
# # # liquid_height=[None] * 32,
# # # blow_out_air_volume=[None] * 32,
# # # mix_times=3,
# # # mix_vol=50,
# # # spread="wide",
# # # ))
# # print(json.dumps(handler._unilabos_backend.steps_todo_list, indent=2)) # Print matrix info
# # # input("pick_up_tips add step")
#asyncio.run(handler.run_protocol()) # Run the protocol
# # # input("Running protocol...")
# # # input("Press Enter to continue...") # Wait for user input before proceeding
# # # print("PRCXI9300Handler initialized with deck and host settings.")
# 一些推荐版位组合的测试样例:
# 一些推荐版位组合的测试样例:
# 一些推荐版位组合的测试样例:
# 一些推荐版位组合的测试样例:
with open("prcxi_material.json", "r") as f: with open("prcxi_material.json", "r") as f:
material_info = json.load(f) material_info = json.load(f)
layout = DefaultLayout("PRCXI9320") layout = DefaultLayout("PRCXI9320")
layout.add_lab_resource(material_info) layout.add_lab_resource(material_info)
MatrixLayout_1, dict_1 = layout.recommend_layout( MatrixLayout_1, dict_1 = layout.recommend_layout([
[ ("reagent_1", "96 细胞培养皿", 3),
("reagent_1", "96 细胞培养皿", 3), ("reagent_2", "12道储液槽", 1),
("reagent_2", "12道储液槽", 1), ("reagent_3", "200μL Tip头", 7),
("reagent_3", "200μL Tip头", 7), ("reagent_4", "10μL加长 Tip头", 1),
("reagent_4", "10μL加长 Tip头", 1), ])
]
)
print(dict_1) print(dict_1)
MatrixLayout_2, dict_2 = layout.recommend_layout( MatrixLayout_2, dict_2 = layout.recommend_layout([
[ ("reagent_1", "96深孔板", 4),
("reagent_1", "96深孔板", 4), ("reagent_2", "12道储液槽", 1),
("reagent_2", "12道储液槽", 1), ("reagent_3", "200μL Tip头", 1),
("reagent_3", "200μL Tip头", 1), ("reagent_4", "10μL加长 Tip头", 1),
("reagent_4", "10μL加长 Tip头", 1), ])
]
)
# with open("prcxi_material.json", "r") as f: # with open("prcxi_material.json", "r") as f:
# material_info = json.load(f) # material_info = json.load(f)

View File

@@ -8,8 +8,6 @@ import serial.tools.list_ports
from serial import Serial from serial import Serial
from serial.serialutil import SerialException from serial.serialutil import SerialException
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
class RunzeSyringePumpMode(Enum): class RunzeSyringePumpMode(Enum):
Normal = 0 Normal = 0
@@ -79,8 +77,6 @@ class RunzeSyringePumpInfo:
class RunzeSyringePumpAsync: class RunzeSyringePumpAsync:
_ros_node: BaseROS2DeviceNode
def __init__(self, port: str, address: str = "1", volume: float = 25000, mode: RunzeSyringePumpMode = None): def __init__(self, port: str, address: str = "1", volume: float = 25000, mode: RunzeSyringePumpMode = None):
self.port = port self.port = port
self.address = address self.address = address
@@ -106,9 +102,6 @@ class RunzeSyringePumpAsync:
self._run_future: Optional[Future[Any]] = None self._run_future: Optional[Future[Any]] = None
self._run_lock = Lock() self._run_lock = Lock()
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
def _adjust_total_steps(self): def _adjust_total_steps(self):
self.total_steps = 6000 if self.mode == RunzeSyringePumpMode.Normal else 48000 self.total_steps = 6000 if self.mode == RunzeSyringePumpMode.Normal else 48000
self.total_steps_vel = 48000 if self.mode == RunzeSyringePumpMode.AccuratePosVel else 6000 self.total_steps_vel = 48000 if self.mode == RunzeSyringePumpMode.AccuratePosVel else 6000
@@ -189,7 +182,7 @@ class RunzeSyringePumpAsync:
try: try:
await self._query(command) await self._query(command)
while True: while True:
await self._ros_node.sleep(0.5) # Wait for 0.5 seconds before polling again await asyncio.sleep(0.5) # Wait for 0.5 seconds before polling again
status = await self.query_device_status() status = await self.query_device_status()
if status == '`': if status == '`':
@@ -371,7 +364,7 @@ class RunzeSyringePumpAsync:
if self._read_task: if self._read_task:
raise RunzeSyringePumpConnectionError raise RunzeSyringePumpConnectionError
self._read_task = self._ros_node.create_task(self._read_loop()) self._read_task = asyncio.create_task(self._read_loop())
try: try:
await self.query_device_status() await self.query_device_status()

View File

@@ -3,13 +3,9 @@ import logging
import time as time_module import time as time_module
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
class VirtualCentrifuge: class VirtualCentrifuge:
"""Virtual centrifuge device - 简化版,只保留核心功能""" """Virtual centrifuge device - 简化版,只保留核心功能"""
_ros_node: BaseROS2DeviceNode
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs): def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
# 处理可能的不同调用方式 # 处理可能的不同调用方式
@@ -36,9 +32,6 @@ class VirtualCentrifuge:
for key, value in kwargs.items(): for key, value in kwargs.items():
if key not in skip_keys and not hasattr(self, key): if key not in skip_keys and not hasattr(self, key):
setattr(self, key, value) setattr(self, key, value)
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
async def initialize(self) -> bool: async def initialize(self) -> bool:
"""Initialize virtual centrifuge""" """Initialize virtual centrifuge"""
@@ -139,7 +132,7 @@ class VirtualCentrifuge:
break break
# 每秒更新一次 # 每秒更新一次
await self._ros_node.sleep(1.0) await asyncio.sleep(1.0)
# 离心完成 # 离心完成
self.data.update({ self.data.update({

View File

@@ -2,13 +2,9 @@ import asyncio
import logging import logging
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
class VirtualColumn: class VirtualColumn:
"""Virtual column device for RunColumn protocol 🏛️""" """Virtual column device for RunColumn protocol 🏛️"""
_ros_node: BaseROS2DeviceNode
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
# 处理可能的不同调用方式 # 处理可能的不同调用方式
if device_id is None and 'id' in kwargs: if device_id is None and 'id' in kwargs:
@@ -32,9 +28,6 @@ class VirtualColumn:
print(f"🏛️ === 虚拟色谱柱 {self.device_id} 已创建 === ✨") print(f"🏛️ === 虚拟色谱柱 {self.device_id} 已创建 === ✨")
print(f"📏 柱参数: 流速={self._max_flow_rate}mL/min | 长度={self._column_length}cm | 直径={self._column_diameter}cm 🔬") print(f"📏 柱参数: 流速={self._max_flow_rate}mL/min | 长度={self._column_length}cm | 直径={self._column_diameter}cm 🔬")
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
async def initialize(self) -> bool: async def initialize(self) -> bool:
"""Initialize virtual column 🚀""" """Initialize virtual column 🚀"""
self.logger.info(f"🔧 初始化虚拟色谱柱 {self.device_id}") self.logger.info(f"🔧 初始化虚拟色谱柱 {self.device_id}")
@@ -108,7 +101,7 @@ class VirtualColumn:
step_time = separation_time / steps step_time = separation_time / steps
for i in range(steps): for i in range(steps):
await self._ros_node.sleep(step_time) await asyncio.sleep(step_time)
progress = (i + 1) / steps * 100 progress = (i + 1) / steps * 100
volume_processed = (i + 1) * 5.0 # 假设每步处理5mL volume_processed = (i + 1) * 5.0 # 假设每步处理5mL

View File

@@ -4,76 +4,70 @@ import time as time_module
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from unilabos.compile.utils.vessel_parser import get_vessel from unilabos.compile.utils.vessel_parser import get_vessel
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
class VirtualFilter: class VirtualFilter:
"""Virtual filter device - 完全按照 Filter.action 规范 🌊""" """Virtual filter device - 完全按照 Filter.action 规范 🌊"""
_ros_node: BaseROS2DeviceNode
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs): def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
if device_id is None and "id" in kwargs: if device_id is None and 'id' in kwargs:
device_id = kwargs.pop("id") device_id = kwargs.pop('id')
if config is None and "config" in kwargs: if config is None and 'config' in kwargs:
config = kwargs.pop("config") config = kwargs.pop('config')
self.device_id = device_id or "unknown_filter" self.device_id = device_id or "unknown_filter"
self.config = config or {} self.config = config or {}
self.logger = logging.getLogger(f"VirtualFilter.{self.device_id}") self.logger = logging.getLogger(f"VirtualFilter.{self.device_id}")
self.data = {} self.data = {}
# 从config或kwargs中获取配置参数 # 从config或kwargs中获取配置参数
self.port = self.config.get("port") or kwargs.get("port", "VIRTUAL") self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL')
self._max_temp = self.config.get("max_temp") or kwargs.get("max_temp", 100.0) self._max_temp = self.config.get('max_temp') or kwargs.get('max_temp', 100.0)
self._max_stir_speed = self.config.get("max_stir_speed") or kwargs.get("max_stir_speed", 1000.0) self._max_stir_speed = self.config.get('max_stir_speed') or kwargs.get('max_stir_speed', 1000.0)
self._max_volume = self.config.get("max_volume") or kwargs.get("max_volume", 500.0) self._max_volume = self.config.get('max_volume') or kwargs.get('max_volume', 500.0)
# 处理其他kwargs参数 # 处理其他kwargs参数
skip_keys = {"port", "max_temp", "max_stir_speed", "max_volume"} skip_keys = {'port', 'max_temp', 'max_stir_speed', 'max_volume'}
for key, value in kwargs.items(): for key, value in kwargs.items():
if key not in skip_keys and not hasattr(self, key): if key not in skip_keys and not hasattr(self, key):
setattr(self, key, value) setattr(self, key, value)
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
async def initialize(self) -> bool: async def initialize(self) -> bool:
"""Initialize virtual filter 🚀""" """Initialize virtual filter 🚀"""
self.logger.info(f"🔧 初始化虚拟过滤器 {self.device_id}") self.logger.info(f"🔧 初始化虚拟过滤器 {self.device_id}")
# 按照 Filter.action 的 feedback 字段初始化 # 按照 Filter.action 的 feedback 字段初始化
self.data.update( self.data.update({
{ "status": "Idle",
"status": "Idle", "progress": 0.0, # Filter.action feedback
"progress": 0.0, # Filter.action feedback "current_temp": 25.0, # Filter.action feedback
"current_temp": 25.0, # Filter.action feedback "filtered_volume": 0.0, # Filter.action feedback
"filtered_volume": 0.0, # Filter.action feedback "message": "Ready for filtration"
"message": "Ready for filtration", })
}
)
self.logger.info(f"✅ 过滤器 {self.device_id} 初始化完成 🌊") self.logger.info(f"✅ 过滤器 {self.device_id} 初始化完成 🌊")
return True return True
async def cleanup(self) -> bool: async def cleanup(self) -> bool:
"""Cleanup virtual filter 🧹""" """Cleanup virtual filter 🧹"""
self.logger.info(f"🧹 清理虚拟过滤器 {self.device_id} 🔚") self.logger.info(f"🧹 清理虚拟过滤器 {self.device_id} 🔚")
self.data.update({"status": "Offline"}) self.data.update({
"status": "Offline"
})
self.logger.info(f"✅ 过滤器 {self.device_id} 清理完成 💤") self.logger.info(f"✅ 过滤器 {self.device_id} 清理完成 💤")
return True return True
async def filter( async def filter(
self, self,
vessel: dict, vessel: dict,
filtrate_vessel: dict = {}, filtrate_vessel: dict = {},
stir: bool = False, stir: bool = False,
stir_speed: float = 300.0, stir_speed: float = 300.0,
temp: float = 25.0, temp: float = 25.0,
continue_heatchill: bool = False, continue_heatchill: bool = False,
volume: float = 0.0, volume: float = 0.0
) -> bool: ) -> bool:
"""Execute filter action - 完全按照 Filter.action 参数 🌊""" """Execute filter action - 完全按照 Filter.action 参数 🌊"""
vessel_id, _ = get_vessel(vessel) vessel_id, _ = get_vessel(vessel)
@@ -85,52 +79,59 @@ class VirtualFilter:
temp = 25.0 # 0度自动设置为室温 temp = 25.0 # 0度自动设置为室温
self.logger.info(f"🌡️ 温度自动调整: {original_temp}°C → {temp}°C (室温) 🏠") self.logger.info(f"🌡️ 温度自动调整: {original_temp}°C → {temp}°C (室温) 🏠")
elif temp < 4.0: elif temp < 4.0:
temp = 4.0 # 小于4度自动设置为4度 temp = 4.0 # 小于4度自动设置为4度
self.logger.info(f"🌡️ 温度自动调整: {original_temp}°C → {temp}°C (最低温度) ❄️") self.logger.info(f"🌡️ 温度自动调整: {original_temp}°C → {temp}°C (最低温度) ❄️")
self.logger.info(f"🌊 开始过滤操作: {vessel_id}{filtrate_vessel_id} 🚰") self.logger.info(f"🌊 开始过滤操作: {vessel_id}{filtrate_vessel_id} 🚰")
self.logger.info(f" 🌪️ 搅拌: {stir} ({stir_speed} RPM)") self.logger.info(f" 🌪️ 搅拌: {stir} ({stir_speed} RPM)")
self.logger.info(f" 🌡️ 温度: {temp}°C") self.logger.info(f" 🌡️ 温度: {temp}°C")
self.logger.info(f" 💧 体积: {volume}mL") self.logger.info(f" 💧 体积: {volume}mL")
self.logger.info(f" 🔥 保持加热: {continue_heatchill}") self.logger.info(f" 🔥 保持加热: {continue_heatchill}")
# 验证参数 # 验证参数
if temp > self._max_temp or temp < 4.0: if temp > self._max_temp or temp < 4.0:
error_msg = f"🌡️ 温度 {temp}°C 超出范围 (4-{self._max_temp}°C) ⚠️" error_msg = f"🌡️ 温度 {temp}°C 超出范围 (4-{self._max_temp}°C) ⚠️"
self.logger.error(f"{error_msg}") self.logger.error(f"{error_msg}")
self.data.update({"status": f"Error: 温度超出范围 ⚠️", "message": error_msg}) self.data.update({
"status": f"Error: 温度超出范围 ⚠️",
"message": error_msg
})
return False return False
if stir and stir_speed > self._max_stir_speed: if stir and stir_speed > self._max_stir_speed:
error_msg = f"🌪️ 搅拌速度 {stir_speed} RPM 超出范围 (0-{self._max_stir_speed} RPM) ⚠️" error_msg = f"🌪️ 搅拌速度 {stir_speed} RPM 超出范围 (0-{self._max_stir_speed} RPM) ⚠️"
self.logger.error(f"{error_msg}") self.logger.error(f"{error_msg}")
self.data.update({"status": f"Error: 搅拌速度超出范围 ⚠️", "message": error_msg}) self.data.update({
"status": f"Error: 搅拌速度超出范围 ⚠️",
"message": error_msg
})
return False return False
if volume > self._max_volume: if volume > self._max_volume:
error_msg = f"💧 过滤体积 {volume} mL 超出范围 (0-{self._max_volume} mL) ⚠️" error_msg = f"💧 过滤体积 {volume} mL 超出范围 (0-{self._max_volume} mL) ⚠️"
self.logger.error(f"{error_msg}") self.logger.error(f"{error_msg}")
self.data.update({"status": f"Error", "message": error_msg}) self.data.update({
"status": f"Error",
"message": error_msg
})
return False return False
# 开始过滤 # 开始过滤
filter_volume = volume if volume > 0 else 50.0 filter_volume = volume if volume > 0 else 50.0
self.logger.info(f"🚀 开始过滤 {filter_volume}mL 液体 💧") self.logger.info(f"🚀 开始过滤 {filter_volume}mL 液体 💧")
self.data.update( self.data.update({
{ "status": f"Running",
"status": f"Running", "current_temp": temp,
"current_temp": temp, "filtered_volume": 0.0,
"filtered_volume": 0.0, "progress": 0.0,
"progress": 0.0, "message": f"🚀 Starting filtration: {vessel_id}{filtrate_vessel_id}"
"message": f"🚀 Starting filtration: {vessel_id}{filtrate_vessel_id}", })
}
)
try: try:
# 过滤过程 - 实时更新进度 # 过滤过程 - 实时更新进度
start_time = time_module.time() start_time = time_module.time()
# 根据体积和搅拌估算过滤时间 # 根据体积和搅拌估算过滤时间
base_time = filter_volume / 5.0 # 5mL/s 基础速度 base_time = filter_volume / 5.0 # 5mL/s 基础速度
if stir: if stir:
@@ -139,79 +140,78 @@ class VirtualFilter:
if temp > 50.0: if temp > 50.0:
base_time *= 0.7 # 高温加速过滤 base_time *= 0.7 # 高温加速过滤
self.logger.info(f"🔥 高温加速过滤预计时间减少30% ⚡") self.logger.info(f"🔥 高温加速过滤预计时间减少30% ⚡")
filter_time = max(base_time, 10.0) # 最少10秒 filter_time = max(base_time, 10.0) # 最少10秒
self.logger.info(f"⏱️ 预计过滤时间: {filter_time:.1f}秒 ⌛") self.logger.info(f"⏱️ 预计过滤时间: {filter_time:.1f}秒 ⌛")
while True: while True:
current_time = time_module.time() current_time = time_module.time()
elapsed = current_time - start_time elapsed = current_time - start_time
remaining = max(0, filter_time - elapsed) remaining = max(0, filter_time - elapsed)
progress = min(100.0, (elapsed / filter_time) * 100) progress = min(100.0, (elapsed / filter_time) * 100)
current_filtered = (progress / 100.0) * filter_volume current_filtered = (progress / 100.0) * filter_volume
# 更新状态 - 按照 Filter.action feedback 字段 # 更新状态 - 按照 Filter.action feedback 字段
status_msg = f"🌊 过滤中: {vessel}" status_msg = f"🌊 过滤中: {vessel}"
if stir: if stir:
status_msg += f" | 🌪️ 搅拌: {stir_speed} RPM" status_msg += f" | 🌪️ 搅拌: {stir_speed} RPM"
status_msg += f" | 🌡️ {temp}°C | 📊 {progress:.1f}% | 💧 已过滤: {current_filtered:.1f}mL" status_msg += f" | 🌡️ {temp}°C | 📊 {progress:.1f}% | 💧 已过滤: {current_filtered:.1f}mL"
self.data.update( self.data.update({
{ "progress": progress, # Filter.action feedback
"progress": progress, # Filter.action feedback "current_temp": temp, # Filter.action feedback
"current_temp": temp, # Filter.action feedback "filtered_volume": current_filtered, # Filter.action feedback
"filtered_volume": current_filtered, # Filter.action feedback "status": "Running",
"status": "Running", "message": f"🌊 Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered"
"message": f"🌊 Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered", })
}
)
# 进度日志每25%打印一次) # 进度日志每25%打印一次)
if progress >= 25 and progress % 25 < 1: if progress >= 25 and progress % 25 < 1:
self.logger.info(f"📊 过滤进度: {progress:.0f}% | 💧 {current_filtered:.1f}mL 完成 ✨") self.logger.info(f"📊 过滤进度: {progress:.0f}% | 💧 {current_filtered:.1f}mL 完成 ✨")
if remaining <= 0: if remaining <= 0:
break break
await self._ros_node.sleep(1.0) await asyncio.sleep(1.0)
# 过滤完成 # 过滤完成
final_temp = temp if continue_heatchill else 25.0 final_temp = temp if continue_heatchill else 25.0
final_status = f"✅ 过滤完成: {vessel} | 💧 {filter_volume}mL → {filtrate_vessel}" final_status = f"✅ 过滤完成: {vessel} | 💧 {filter_volume}mL → {filtrate_vessel}"
if continue_heatchill: if continue_heatchill:
final_status += " | 🔥 继续加热搅拌" final_status += " | 🔥 继续加热搅拌"
self.logger.info(f"🔥 继续保持加热搅拌状态 🌪️") self.logger.info(f"🔥 继续保持加热搅拌状态 🌪️")
self.data.update( self.data.update({
{ "status": final_status,
"status": final_status, "progress": 100.0, # Filter.action feedback
"progress": 100.0, # Filter.action feedback "current_temp": final_temp, # Filter.action feedback
"current_temp": final_temp, # Filter.action feedback "filtered_volume": filter_volume, # Filter.action feedback
"filtered_volume": filter_volume, # Filter.action feedback "message": f"✅ Filtration completed: {filter_volume}mL filtered from {vessel_id}"
"message": f"✅ Filtration completed: {filter_volume}mL filtered from {vessel_id}", })
}
)
self.logger.info(f"🎉 过滤完成! 💧 {filter_volume}mL 从 {vessel_id} 过滤到 {filtrate_vessel_id}") self.logger.info(f"🎉 过滤完成! 💧 {filter_volume}mL 从 {vessel_id} 过滤到 {filtrate_vessel_id}")
self.logger.info(f"📊 最终状态: 温度 {final_temp}°C | 进度 100% | 体积 {filter_volume}mL 🏁") self.logger.info(f"📊 最终状态: 温度 {final_temp}°C | 进度 100% | 体积 {filter_volume}mL 🏁")
return True return True
except Exception as e: except Exception as e:
error_msg = f"过滤过程中发生错误: {str(e)} 💥" error_msg = f"过滤过程中发生错误: {str(e)} 💥"
self.logger.error(f"{error_msg}") self.logger.error(f"{error_msg}")
self.data.update({"status": f"Error", "message": f"❌ Filtration failed: {str(e)}"}) self.data.update({
"status": f"Error",
"message": f"❌ Filtration failed: {str(e)}"
})
return False return False
# === 核心状态属性 - 按照 Filter.action feedback 字段 === # === 核心状态属性 - 按照 Filter.action feedback 字段 ===
@property @property
def status(self) -> str: def status(self) -> str:
return self.data.get("status", "❓ Unknown") return self.data.get("status", "❓ Unknown")
@property @property
def progress(self) -> float: def progress(self) -> float:
"""Filter.action feedback 字段 📊""" """Filter.action feedback 字段 📊"""
return self.data.get("progress", 0.0) return self.data.get("progress", 0.0)
@property @property
def current_temp(self) -> float: def current_temp(self) -> float:
"""Filter.action feedback 字段 🌡️""" """Filter.action feedback 字段 🌡️"""
@@ -230,15 +230,15 @@ class VirtualFilter:
@property @property
def message(self) -> str: def message(self) -> str:
return self.data.get("message", "") return self.data.get("message", "")
@property @property
def max_temp(self) -> float: def max_temp(self) -> float:
return self._max_temp return self._max_temp
@property @property
def max_stir_speed(self) -> float: def max_stir_speed(self) -> float:
return self._max_stir_speed return self._max_stir_speed
@property @property
def max_volume(self) -> float: def max_volume(self) -> float:
return self._max_volume return self._max_volume

View File

@@ -3,13 +3,9 @@ import logging
import time as time_module # 重命名time模块避免与参数冲突 import time as time_module # 重命名time模块避免与参数冲突
from typing import Dict, Any from typing import Dict, Any
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
class VirtualHeatChill: class VirtualHeatChill:
"""Virtual heat chill device for HeatChillProtocol testing 🌡️""" """Virtual heat chill device for HeatChillProtocol testing 🌡️"""
_ros_node: BaseROS2DeviceNode
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
# 处理可能的不同调用方式 # 处理可能的不同调用方式
if device_id is None and 'id' in kwargs: if device_id is None and 'id' in kwargs:
@@ -39,9 +35,6 @@ class VirtualHeatChill:
print(f"🌡️ === 虚拟温控设备 {self.device_id} 已创建 === ✨") print(f"🌡️ === 虚拟温控设备 {self.device_id} 已创建 === ✨")
print(f"🔥 温度范围: {self._min_temp}°C ~ {self._max_temp}°C | 🌪️ 最大搅拌: {self._max_stir_speed} RPM") print(f"🔥 温度范围: {self._min_temp}°C ~ {self._max_temp}°C | 🌪️ 最大搅拌: {self._max_stir_speed} RPM")
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
async def initialize(self) -> bool: async def initialize(self) -> bool:
"""Initialize virtual heat chill 🚀""" """Initialize virtual heat chill 🚀"""
self.logger.info(f"🔧 初始化虚拟温控设备 {self.device_id}") self.logger.info(f"🔧 初始化虚拟温控设备 {self.device_id}")
@@ -184,7 +177,7 @@ class VirtualHeatChill:
break break
# 等待1秒后再次检查 # 等待1秒后再次检查
await self._ros_node.sleep(1.0) await asyncio.sleep(1.0)
# 操作完成 # 操作完成
final_stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else "" final_stir_info = f" | 🌪️ 搅拌: {stir_speed} RPM" if stir else ""

View File

@@ -3,19 +3,13 @@ import logging
import time as time_module import time as time_module
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
def debug_print(message): def debug_print(message):
"""调试输出 🔍""" """调试输出 🔍"""
print(f"🌪️ [ROTAVAP] {message}", flush=True) print(f"🌪️ [ROTAVAP] {message}", flush=True)
class VirtualRotavap: class VirtualRotavap:
"""Virtual rotary evaporator device - 简化版,只保留核心功能 🌪️""" """Virtual rotary evaporator device - 简化版,只保留核心功能 🌪️"""
_ros_node: BaseROS2DeviceNode
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs): def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
# 处理可能的不同调用方式 # 处理可能的不同调用方式
if device_id is None and "id" in kwargs: if device_id is None and "id" in kwargs:
@@ -44,65 +38,56 @@ class VirtualRotavap:
print(f"🌪️ === 虚拟旋转蒸发仪 {self.device_id} 已创建 === ✨") print(f"🌪️ === 虚拟旋转蒸发仪 {self.device_id} 已创建 === ✨")
print(f"🔥 温度范围: 10°C ~ {self._max_temp}°C | 🌀 转速范围: 10 ~ {self._max_rotation_speed} RPM") print(f"🔥 温度范围: 10°C ~ {self._max_temp}°C | 🌀 转速范围: 10 ~ {self._max_rotation_speed} RPM")
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
async def initialize(self) -> bool: async def initialize(self) -> bool:
"""Initialize virtual rotary evaporator 🚀""" """Initialize virtual rotary evaporator 🚀"""
self.logger.info(f"🔧 初始化虚拟旋转蒸发仪 {self.device_id}") self.logger.info(f"🔧 初始化虚拟旋转蒸发仪 {self.device_id}")
# 只保留核心状态 # 只保留核心状态
self.data.update( self.data.update({
{ "status": "🏠 待机中",
"status": "🏠 待机中", "rotavap_state": "Ready", # Ready, Evaporating, Completed, Error
"rotavap_state": "Ready", # Ready, Evaporating, Completed, Error "current_temp": 25.0,
"current_temp": 25.0, "target_temp": 25.0,
"target_temp": 25.0, "rotation_speed": 0.0,
"rotation_speed": 0.0, "vacuum_pressure": 1.0, # 大气压
"vacuum_pressure": 1.0, # 大气压 "evaporated_volume": 0.0,
"evaporated_volume": 0.0, "progress": 0.0,
"progress": 0.0, "remaining_time": 0.0,
"remaining_time": 0.0, "message": "🌪️ Ready for evaporation"
"message": "🌪️ Ready for evaporation", })
}
)
self.logger.info(f"✅ 旋转蒸发仪 {self.device_id} 初始化完成 🌪️") self.logger.info(f"✅ 旋转蒸发仪 {self.device_id} 初始化完成 🌪️")
self.logger.info( self.logger.info(f"📊 设备规格: 温度范围 10°C ~ {self._max_temp}°C | 转速范围 10 ~ {self._max_rotation_speed} RPM")
f"📊 设备规格: 温度范围 10°C ~ {self._max_temp}°C | 转速范围 10 ~ {self._max_rotation_speed} RPM"
)
return True return True
async def cleanup(self) -> bool: async def cleanup(self) -> bool:
"""Cleanup virtual rotary evaporator 🧹""" """Cleanup virtual rotary evaporator 🧹"""
self.logger.info(f"🧹 清理虚拟旋转蒸发仪 {self.device_id} 🔚") self.logger.info(f"🧹 清理虚拟旋转蒸发仪 {self.device_id} 🔚")
self.data.update( self.data.update({
{ "status": "💤 离线",
"status": "💤 离线", "rotavap_state": "Offline",
"rotavap_state": "Offline", "current_temp": 25.0,
"current_temp": 25.0, "rotation_speed": 0.0,
"rotation_speed": 0.0, "vacuum_pressure": 1.0,
"vacuum_pressure": 1.0, "message": "💤 System offline"
"message": "💤 System offline", })
}
)
self.logger.info(f"✅ 旋转蒸发仪 {self.device_id} 清理完成 💤") self.logger.info(f"✅ 旋转蒸发仪 {self.device_id} 清理完成 💤")
return True return True
async def evaporate( async def evaporate(
self, self,
vessel: str, vessel: str,
pressure: float = 0.1, pressure: float = 0.1,
temp: float = 60.0, temp: float = 60.0,
time: float = 180.0, time: float = 180.0,
stir_speed: float = 100.0, stir_speed: float = 100.0,
solvent: str = "", solvent: str = "",
**kwargs, **kwargs
) -> bool: ) -> bool:
"""Execute evaporate action - 简化版 🌪️""" """Execute evaporate action - 简化版 🌪️"""
# 🔧 新增确保time参数是数值类型 # 🔧 新增确保time参数是数值类型
if isinstance(time, str): if isinstance(time, str):
try: try:
@@ -113,31 +98,31 @@ class VirtualRotavap:
elif not isinstance(time, (int, float)): elif not isinstance(time, (int, float)):
self.logger.error(f"❌ 时间参数类型无效: {type(time)}使用默认值180.0秒") self.logger.error(f"❌ 时间参数类型无效: {type(time)}使用默认值180.0秒")
time = 180.0 time = 180.0
# 确保time是float类型; 并加速 # 确保time是float类型; 并加速
time = float(time) / 10.0 time = float(time) / 10.0
# 🔧 简化处理如果vessel就是设备自己直接操作 # 🔧 简化处理如果vessel就是设备自己直接操作
if vessel == self.device_id: if vessel == self.device_id:
debug_print(f"🎯 在设备 {self.device_id} 上直接执行蒸发操作") debug_print(f"🎯 在设备 {self.device_id} 上直接执行蒸发操作")
actual_vessel = self.device_id actual_vessel = self.device_id
else: else:
actual_vessel = vessel actual_vessel = vessel
# 参数预处理 # 参数预处理
if solvent: if solvent:
self.logger.info(f"🧪 识别到溶剂: {solvent}") self.logger.info(f"🧪 识别到溶剂: {solvent}")
# 根据溶剂调整参数 # 根据溶剂调整参数
solvent_lower = solvent.lower() solvent_lower = solvent.lower()
if any(s in solvent_lower for s in ["water", "aqueous"]): if any(s in solvent_lower for s in ['water', 'aqueous']):
temp = max(temp, 80.0) temp = max(temp, 80.0)
pressure = max(pressure, 0.2) pressure = max(pressure, 0.2)
self.logger.info(f"💧 水系溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar") self.logger.info(f"💧 水系溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar")
elif any(s in solvent_lower for s in ["ethanol", "methanol", "acetone"]): elif any(s in solvent_lower for s in ['ethanol', 'methanol', 'acetone']):
temp = min(temp, 50.0) temp = min(temp, 50.0)
pressure = min(pressure, 0.05) pressure = min(pressure, 0.05)
self.logger.info(f"⚡ 易挥发溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar") self.logger.info(f"⚡ 易挥发溶剂:调整参数 → 温度 {temp}°C, 压力 {pressure} bar")
self.logger.info(f"🌪️ 开始蒸发操作: {actual_vessel}") self.logger.info(f"🌪️ 开始蒸发操作: {actual_vessel}")
self.logger.info(f" 🥽 容器: {actual_vessel}") self.logger.info(f" 🥽 容器: {actual_vessel}")
self.logger.info(f" 🌡️ 温度: {temp}°C") self.logger.info(f" 🌡️ 温度: {temp}°C")
@@ -146,140 +131,126 @@ class VirtualRotavap:
self.logger.info(f" 🌀 转速: {stir_speed} RPM") self.logger.info(f" 🌀 转速: {stir_speed} RPM")
if solvent: if solvent:
self.logger.info(f" 🧪 溶剂: {solvent}") self.logger.info(f" 🧪 溶剂: {solvent}")
# 验证参数 # 验证参数
if temp > self._max_temp or temp < 10.0: if temp > self._max_temp or temp < 10.0:
error_msg = f"🌡️ 温度 {temp}°C 超出范围 (10-{self._max_temp}°C) ⚠️" error_msg = f"🌡️ 温度 {temp}°C 超出范围 (10-{self._max_temp}°C) ⚠️"
self.logger.error(f"{error_msg}") self.logger.error(f"{error_msg}")
self.data.update( self.data.update({
{ "status": f"❌ 错误: 温度超出范围",
"status": f"❌ 错误: 温度超出范围", "rotavap_state": "Error",
"rotavap_state": "Error", "current_temp": 25.0,
"current_temp": 25.0, "progress": 0.0,
"progress": 0.0, "evaporated_volume": 0.0,
"evaporated_volume": 0.0, "message": error_msg
"message": error_msg, })
}
)
return False return False
if stir_speed > self._max_rotation_speed or stir_speed < 10.0: if stir_speed > self._max_rotation_speed or stir_speed < 10.0:
error_msg = f"🌀 旋转速度 {stir_speed} RPM 超出范围 (10-{self._max_rotation_speed} RPM) ⚠️" error_msg = f"🌀 旋转速度 {stir_speed} RPM 超出范围 (10-{self._max_rotation_speed} RPM) ⚠️"
self.logger.error(f"{error_msg}") self.logger.error(f"{error_msg}")
self.data.update( self.data.update({
{ "status": f"❌ 错误: 转速超出范围",
"status": f"❌ 错误: 转速超出范围", "rotavap_state": "Error",
"rotavap_state": "Error", "current_temp": 25.0,
"current_temp": 25.0, "progress": 0.0,
"progress": 0.0, "evaporated_volume": 0.0,
"evaporated_volume": 0.0, "message": error_msg
"message": error_msg, })
}
)
return False return False
if pressure < 0.01 or pressure > 1.0: if pressure < 0.01 or pressure > 1.0:
error_msg = f"💨 真空度 {pressure} bar 超出范围 (0.01-1.0 bar) ⚠️" error_msg = f"💨 真空度 {pressure} bar 超出范围 (0.01-1.0 bar) ⚠️"
self.logger.error(f"{error_msg}") self.logger.error(f"{error_msg}")
self.data.update( self.data.update({
{ "status": f"❌ 错误: 压力超出范围",
"status": f"❌ 错误: 压力超出范围", "rotavap_state": "Error",
"rotavap_state": "Error", "current_temp": 25.0,
"current_temp": 25.0, "progress": 0.0,
"progress": 0.0, "evaporated_volume": 0.0,
"evaporated_volume": 0.0, "message": error_msg
"message": error_msg, })
}
)
return False return False
# 开始蒸发 - 🔧 现在time已经确保是float类型 # 开始蒸发 - 🔧 现在time已经确保是float类型
self.logger.info(f"🚀 启动蒸发程序! 预计用时 {time/60:.1f}分钟 ⏱️") self.logger.info(f"🚀 启动蒸发程序! 预计用时 {time/60:.1f}分钟 ⏱️")
self.data.update( self.data.update({
{ "status": f"🌪️ 蒸发中: {actual_vessel}",
"status": f"🌪️ 蒸发中: {actual_vessel}", "rotavap_state": "Evaporating",
"rotavap_state": "Evaporating", "current_temp": temp,
"current_temp": temp, "target_temp": temp,
"target_temp": temp, "rotation_speed": stir_speed,
"rotation_speed": stir_speed, "vacuum_pressure": pressure,
"vacuum_pressure": pressure, "remaining_time": time,
"remaining_time": time, "progress": 0.0,
"progress": 0.0, "evaporated_volume": 0.0,
"evaporated_volume": 0.0, "message": f"🌪️ Evaporating {actual_vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM"
"message": f"🌪️ Evaporating {actual_vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM", })
}
)
try: try:
# 蒸发过程 - 实时更新进度 # 蒸发过程 - 实时更新进度
start_time = time_module.time() start_time = time_module.time()
total_time = time total_time = time
last_logged_progress = 0 last_logged_progress = 0
while True: while True:
current_time = time_module.time() current_time = time_module.time()
elapsed = current_time - start_time elapsed = current_time - start_time
remaining = max(0, total_time - elapsed) remaining = max(0, total_time - elapsed)
progress = min(100.0, (elapsed / total_time) * 100) progress = min(100.0, (elapsed / total_time) * 100)
# 模拟蒸发体积 - 根据溶剂类型调整 # 模拟蒸发体积 - 根据溶剂类型调整
if solvent and any(s in solvent.lower() for s in ["water", "aqueous"]): if solvent and any(s in solvent.lower() for s in ['water', 'aqueous']):
evaporated_vol = progress * 0.6 # 水系溶剂蒸发慢 evaporated_vol = progress * 0.6 # 水系溶剂蒸发慢
elif solvent and any(s in solvent.lower() for s in ["ethanol", "methanol", "acetone"]): elif solvent and any(s in solvent.lower() for s in ['ethanol', 'methanol', 'acetone']):
evaporated_vol = progress * 1.0 # 易挥发溶剂蒸发快 evaporated_vol = progress * 1.0 # 易挥发溶剂蒸发快
else: else:
evaporated_vol = progress * 0.8 # 默认蒸发量 evaporated_vol = progress * 0.8 # 默认蒸发量
# 🔧 更新状态 - 确保包含所有必需字段 # 🔧 更新状态 - 确保包含所有必需字段
status_msg = f"🌪️ 蒸发中: {actual_vessel} | 🌡️ {temp}°C | 💨 {pressure} bar | 🌀 {stir_speed} RPM | 📊 {progress:.1f}% | ⏰ 剩余: {remaining:.0f}s" status_msg = f"🌪️ 蒸发中: {actual_vessel} | 🌡️ {temp}°C | 💨 {pressure} bar | 🌀 {stir_speed} RPM | 📊 {progress:.1f}% | ⏰ 剩余: {remaining:.0f}s"
self.data.update( self.data.update({
{ "remaining_time": remaining,
"remaining_time": remaining, "progress": progress,
"progress": progress, "evaporated_volume": evaporated_vol,
"evaporated_volume": evaporated_vol, "current_temp": temp,
"current_temp": temp, "status": status_msg,
"status": status_msg, "message": f"🌪️ Evaporating: {progress:.1f}% complete, 💧 {evaporated_vol:.1f}mL evaporated, ⏰ {remaining:.0f}s remaining"
"message": f"🌪️ Evaporating: {progress:.1f}% complete, 💧 {evaporated_vol:.1f}mL evaporated, ⏰ {remaining:.0f}s remaining", })
}
)
# 进度日志每25%打印一次) # 进度日志每25%打印一次)
if progress >= 25 and int(progress) % 25 == 0 and int(progress) != last_logged_progress: if progress >= 25 and int(progress) % 25 == 0 and int(progress) != last_logged_progress:
self.logger.info( self.logger.info(f"📊 蒸发进度: {progress:.0f}% | 💧 已蒸发: {evaporated_vol:.1f}mL | ⏰ 剩余: {remaining:.0f}s ✨")
f"📊 蒸发进度: {progress:.0f}% | 💧 已蒸发: {evaporated_vol:.1f}mL | ⏰ 剩余: {remaining:.0f}s ✨"
)
last_logged_progress = int(progress) last_logged_progress = int(progress)
# 时间到了,退出循环 # 时间到了,退出循环
if remaining <= 0: if remaining <= 0:
break break
# 每秒更新一次 # 每秒更新一次
await self._ros_node.sleep(1.0) await asyncio.sleep(1.0)
# 蒸发完成 # 蒸发完成
if solvent and any(s in solvent.lower() for s in ["water", "aqueous"]): if solvent and any(s in solvent.lower() for s in ['water', 'aqueous']):
final_evaporated = 60.0 # 水系溶剂 final_evaporated = 60.0 # 水系溶剂
elif solvent and any(s in solvent.lower() for s in ["ethanol", "methanol", "acetone"]): elif solvent and any(s in solvent.lower() for s in ['ethanol', 'methanol', 'acetone']):
final_evaporated = 100.0 # 易挥发溶剂 final_evaporated = 100.0 # 易挥发溶剂
else: else:
final_evaporated = 80.0 # 默认 final_evaporated = 80.0 # 默认
self.data.update( self.data.update({
{ "status": f"✅ 蒸发完成: {actual_vessel} | 💧 蒸发量: {final_evaporated:.1f}mL",
"status": f"✅ 蒸发完成: {actual_vessel} | 💧 蒸发量: {final_evaporated:.1f}mL", "rotavap_state": "Completed",
"rotavap_state": "Completed", "evaporated_volume": final_evaporated,
"evaporated_volume": final_evaporated, "progress": 100.0,
"progress": 100.0, "current_temp": temp,
"current_temp": temp, "remaining_time": 0.0,
"remaining_time": 0.0, "rotation_speed": 0.0,
"rotation_speed": 0.0, "vacuum_pressure": 1.0,
"vacuum_pressure": 1.0, "message": f"✅ Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}"
"message": f"✅ Evaporation completed: {final_evaporated}mL evaporated from {actual_vessel}", })
}
)
self.logger.info(f"🎉 蒸发操作完成! ✨") self.logger.info(f"🎉 蒸发操作完成! ✨")
self.logger.info(f"📊 蒸发结果:") self.logger.info(f"📊 蒸发结果:")
@@ -291,26 +262,24 @@ class VirtualRotavap:
self.logger.info(f" ⏱️ 总用时: {total_time:.0f}s") self.logger.info(f" ⏱️ 总用时: {total_time:.0f}s")
if solvent: if solvent:
self.logger.info(f" 🧪 处理溶剂: {solvent} 🏁") self.logger.info(f" 🧪 处理溶剂: {solvent} 🏁")
return True return True
except Exception as e: except Exception as e:
# 出错处理 # 出错处理
error_msg = f"蒸发过程中发生错误: {str(e)} 💥" error_msg = f"蒸发过程中发生错误: {str(e)} 💥"
self.logger.error(f"{error_msg}") self.logger.error(f"{error_msg}")
self.data.update( self.data.update({
{ "status": f"❌ 蒸发错误: {str(e)}",
"status": f"❌ 蒸发错误: {str(e)}", "rotavap_state": "Error",
"rotavap_state": "Error", "current_temp": 25.0,
"current_temp": 25.0, "progress": 0.0,
"progress": 0.0, "evaporated_volume": 0.0,
"evaporated_volume": 0.0, "rotation_speed": 0.0,
"rotation_speed": 0.0, "vacuum_pressure": 1.0,
"vacuum_pressure": 1.0, "message": f"❌ Evaporation failed: {str(e)}"
"message": f"❌ Evaporation failed: {str(e)}", })
}
)
return False return False
# === 核心状态属性 === # === 核心状态属性 ===

View File

@@ -2,13 +2,9 @@ import asyncio
import logging import logging
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
class VirtualSeparator: class VirtualSeparator:
"""Virtual separator device for SeparateProtocol testing""" """Virtual separator device for SeparateProtocol testing"""
_ros_node: BaseROS2DeviceNode
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs): def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
# 处理可能的不同调用方式 # 处理可能的不同调用方式
@@ -39,9 +35,6 @@ class VirtualSeparator:
for key, value in kwargs.items(): for key, value in kwargs.items():
if key not in skip_keys and not hasattr(self, key): if key not in skip_keys and not hasattr(self, key):
setattr(self, key, value) setattr(self, key, value)
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
async def initialize(self) -> bool: async def initialize(self) -> bool:
"""Initialize virtual separator""" """Initialize virtual separator"""
@@ -126,14 +119,14 @@ class VirtualSeparator:
for repeat in range(repeats): for repeat in range(repeats):
# 搅拌阶段 # 搅拌阶段
for progress in range(0, 51, 10): for progress in range(0, 51, 10):
await self._ros_node.sleep(simulation_time / (repeats * 10)) await asyncio.sleep(simulation_time / (repeats * 10))
overall_progress = ((repeat * 100) + (progress * 0.5)) / repeats overall_progress = ((repeat * 100) + (progress * 0.5)) / repeats
self.data["progress"] = overall_progress self.data["progress"] = overall_progress
self.data["message"] = f"{repeat+1}次分离 - 搅拌中 ({progress}%)" self.data["message"] = f"{repeat+1}次分离 - 搅拌中 ({progress}%)"
# 静置分相阶段 # 静置分相阶段
for progress in range(50, 101, 10): for progress in range(50, 101, 10):
await self._ros_node.sleep(simulation_time / (repeats * 10)) await asyncio.sleep(simulation_time / (repeats * 10))
overall_progress = ((repeat * 100) + (progress * 0.5)) / repeats overall_progress = ((repeat * 100) + (progress * 0.5)) / repeats
self.data["progress"] = overall_progress self.data["progress"] = overall_progress
self.data["message"] = f"{repeat+1}次分离 - 静置分相中 ({progress}%)" self.data["message"] = f"{repeat+1}次分离 - 静置分相中 ({progress}%)"

View File

@@ -2,16 +2,11 @@ import time
import asyncio import asyncio
from typing import Union from typing import Union
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
class VirtualSolenoidValve: class VirtualSolenoidValve:
""" """
虚拟电磁阀门 - 简单的开关型阀门,只有开启和关闭两个状态 虚拟电磁阀门 - 简单的开关型阀门,只有开启和关闭两个状态
""" """
_ros_node: BaseROS2DeviceNode
def __init__(self, device_id: str = None, config: dict = None, **kwargs): def __init__(self, device_id: str = None, config: dict = None, **kwargs):
# 从配置中获取参数,提供默认值 # 从配置中获取参数,提供默认值
if config is None: if config is None:
@@ -26,9 +21,6 @@ class VirtualSolenoidValve:
self._status = "Idle" self._status = "Idle"
self._valve_state = "Closed" # "Open" or "Closed" self._valve_state = "Closed" # "Open" or "Closed"
self._is_open = False self._is_open = False
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
async def initialize(self) -> bool: async def initialize(self) -> bool:
"""初始化设备""" """初始化设备"""
@@ -71,7 +63,7 @@ class VirtualSolenoidValve:
self._status = "Busy" self._status = "Busy"
# 模拟阀门响应时间 # 模拟阀门响应时间
await self._ros_node.sleep(self.response_time) await asyncio.sleep(self.response_time)
# 处理不同的命令格式 # 处理不同的命令格式
if isinstance(command, str): if isinstance(command, str):

View File

@@ -3,8 +3,6 @@ import logging
import re import re
from typing import Dict, Any, Optional from typing import Dict, Any, Optional
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
class VirtualSolidDispenser: class VirtualSolidDispenser:
""" """
虚拟固体粉末加样器 - 用于处理 Add Protocol 中的固体试剂添加 ⚗️ 虚拟固体粉末加样器 - 用于处理 Add Protocol 中的固体试剂添加 ⚗️
@@ -15,8 +13,6 @@ class VirtualSolidDispenser:
- 简单反馈:成功/失败 + 消息 📊 - 简单反馈:成功/失败 + 消息 📊
""" """
_ros_node: BaseROS2DeviceNode
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
self.device_id = device_id or "virtual_solid_dispenser" self.device_id = device_id or "virtual_solid_dispenser"
self.config = config or {} self.config = config or {}
@@ -36,9 +32,6 @@ class VirtualSolidDispenser:
print(f"⚗️ === 虚拟固体分配器 {self.device_id} 创建成功! === ✨") print(f"⚗️ === 虚拟固体分配器 {self.device_id} 创建成功! === ✨")
print(f"📊 设备规格: 最大容量 {self.max_capacity}g | 精度 {self.precision}g 🎯") print(f"📊 设备规格: 最大容量 {self.max_capacity}g | 精度 {self.precision}g 🎯")
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
async def initialize(self) -> bool: async def initialize(self) -> bool:
"""初始化固体加样器 🚀""" """初始化固体加样器 🚀"""
self.logger.info(f"🔧 初始化固体分配器 {self.device_id}") self.logger.info(f"🔧 初始化固体分配器 {self.device_id}")
@@ -270,7 +263,7 @@ class VirtualSolidDispenser:
for i in range(steps): for i in range(steps):
progress = (i + 1) / steps * 100 progress = (i + 1) / steps * 100
await self._ros_node.sleep(step_time) await asyncio.sleep(step_time)
if i % 2 == 0: # 每隔一步显示进度 if i % 2 == 0: # 每隔一步显示进度
self.logger.debug(f"📊 加样进度: {progress:.0f}% | {amount_emoji} 正在分配 {reagent}...") self.logger.debug(f"📊 加样进度: {progress:.0f}% | {amount_emoji} 正在分配 {reagent}...")

View File

@@ -3,13 +3,9 @@ import logging
import time as time_module import time as time_module
from typing import Dict, Any from typing import Dict, Any
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
class VirtualStirrer: class VirtualStirrer:
"""Virtual stirrer device for StirProtocol testing - 功能完整版 🌪️""" """Virtual stirrer device for StirProtocol testing - 功能完整版 🌪️"""
_ros_node: BaseROS2DeviceNode
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs): def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
# 处理可能的不同调用方式 # 处理可能的不同调用方式
if device_id is None and 'id' in kwargs: if device_id is None and 'id' in kwargs:
@@ -38,9 +34,6 @@ class VirtualStirrer:
print(f"🌪️ === 虚拟搅拌器 {self.device_id} 已创建 === ✨") print(f"🌪️ === 虚拟搅拌器 {self.device_id} 已创建 === ✨")
print(f"🔧 速度范围: {self._min_speed} ~ {self._max_speed} RPM | 📱 端口: {self.port}") print(f"🔧 速度范围: {self._min_speed} ~ {self._max_speed} RPM | 📱 端口: {self.port}")
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
async def initialize(self) -> bool: async def initialize(self) -> bool:
"""Initialize virtual stirrer 🚀""" """Initialize virtual stirrer 🚀"""
self.logger.info(f"🔧 初始化虚拟搅拌器 {self.device_id}") self.logger.info(f"🔧 初始化虚拟搅拌器 {self.device_id}")
@@ -141,7 +134,7 @@ class VirtualStirrer:
if remaining <= 0: if remaining <= 0:
break break
await self._ros_node.sleep(1.0) await asyncio.sleep(1.0)
self.logger.info(f"✅ 搅拌阶段完成! 🌪️ {stir_speed} RPM × {stir_time}s") self.logger.info(f"✅ 搅拌阶段完成! 🌪️ {stir_speed} RPM × {stir_time}s")
@@ -183,7 +176,7 @@ class VirtualStirrer:
if remaining <= 0: if remaining <= 0:
break break
await self._ros_node.sleep(1.0) await asyncio.sleep(1.0)
self.logger.info(f"✅ 沉降阶段完成! 🛑 静置 {settling_time}s") self.logger.info(f"✅ 沉降阶段完成! 🛑 静置 {settling_time}s")

View File

@@ -4,8 +4,6 @@ from enum import Enum
from typing import Union, Optional from typing import Union, Optional
import logging import logging
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
class VirtualPumpMode(Enum): class VirtualPumpMode(Enum):
Normal = 0 Normal = 0
@@ -16,8 +14,6 @@ class VirtualPumpMode(Enum):
class VirtualTransferPump: class VirtualTransferPump:
"""虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件 🚰""" """虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件 🚰"""
_ros_node: BaseROS2DeviceNode
def __init__(self, device_id: str = None, config: dict = None, **kwargs): def __init__(self, device_id: str = None, config: dict = None, **kwargs):
""" """
初始化虚拟转移泵 初始化虚拟转移泵
@@ -57,9 +53,6 @@ class VirtualTransferPump:
print(f"💨 快速模式: {'启用' if self._fast_mode else '禁用'} | 移动时间: {self._fast_move_time}s | 喷射时间: {self._fast_dispense_time}s") print(f"💨 快速模式: {'启用' if self._fast_mode else '禁用'} | 移动时间: {self._fast_move_time}s | 喷射时间: {self._fast_dispense_time}s")
print(f"📊 最大容量: {self.max_volume}mL | 端口: {self.port}") print(f"📊 最大容量: {self.max_volume}mL | 端口: {self.port}")
def post_init(self, ros_node: BaseROS2DeviceNode):
self._ros_node = ros_node
async def initialize(self) -> bool: async def initialize(self) -> bool:
"""初始化虚拟泵 🚀""" """初始化虚拟泵 🚀"""
self.logger.info(f"🔧 初始化虚拟转移泵 {self.device_id}") self.logger.info(f"🔧 初始化虚拟转移泵 {self.device_id}")
@@ -111,7 +104,7 @@ class VirtualTransferPump:
async def _simulate_operation(self, duration: float): async def _simulate_operation(self, duration: float):
"""模拟操作延时 ⏱️""" """模拟操作延时 ⏱️"""
self._status = "Busy" self._status = "Busy"
await self._ros_node.sleep(duration) await asyncio.sleep(duration)
self._status = "Idle" self._status = "Idle"
def _calculate_duration(self, volume: float, velocity: float = None) -> float: def _calculate_duration(self, volume: float, velocity: float = None) -> float:
@@ -230,7 +223,7 @@ class VirtualTransferPump:
# 等待一小步时间 # 等待一小步时间
if i < steps and step_duration > 0: if i < steps and step_duration > 0:
await self._ros_node.sleep(step_duration) await asyncio.sleep(step_duration)
else: else:
# 移动距离很小,直接完成 # 移动距离很小,直接完成
self._position = target_position self._position = target_position
@@ -348,7 +341,7 @@ class VirtualTransferPump:
# 短暂停顿 # 短暂停顿
self.logger.debug("⏸️ 短暂停顿...") self.logger.debug("⏸️ 短暂停顿...")
await self._ros_node.sleep(0.1) await asyncio.sleep(0.1)
# 排液 # 排液
await self.dispense(volume, dispense_velocity) await self.dispense(volume, dispense_velocity)

View File

@@ -37,7 +37,7 @@ def _initialize_material_system(self, deck_config: Dict[str, Any], children_conf
**定义在**: `workstation_base.py` **定义在**: `workstation_base.py`
**设计目的** **设计目的**
- 提供外部物料系统如Bioyond、LIMS等集成的标准接口 - 提供外部物料系统如Bioyong、LIMS等集成的标准接口
- 双向同步从外部系统同步到本地deck以及将本地变更同步到外部系统 - 双向同步从外部系统同步到本地deck以及将本地变更同步到外部系统
- 处理外部系统的变更通知 - 处理外部系统的变更通知
@@ -59,7 +59,7 @@ async def handle_external_change(self, change_info: Dict[str, Any]) -> bool:
**扩展功能** **扩展功能**
- HTTP报送接收服务集成 - HTTP报送接收服务集成
- 具体工作流实现(液体转移、板洗等) - 具体工作流实现(液体转移、板洗等)
- Bioyond物料系统同步器示例 - Bioyong物料系统同步器示例
- 外部报送处理方法 - 外部报送处理方法
## 技术栈 ## 技术栈
@@ -142,11 +142,11 @@ success = workstation.execute_workflow("liquid_transfer", {
### 3. 外部系统集成 ### 3. 外部系统集成
```python ```python
class BioyondResourceSynchronizer(ResourceSynchronizer): class BioyongResourceSynchronizer(ResourceSynchronizer):
"""Bioyond系统同步器""" """Bioyong系统同步器"""
async def sync_from_external(self) -> bool: async def sync_from_external(self) -> bool:
# 从Bioyond API获取物料 # 从Bioyong API获取物料
external_materials = await self._fetch_bioyong_materials() external_materials = await self._fetch_bioyong_materials()
# 转换并添加到本地deck # 转换并添加到本地deck

View File

@@ -1,7 +0,0 @@
material_name
LiPF6
LiDFOB
DTD
LiFSI
LiPO2F2
1 material_name
2 LiPF6
3 LiDFOB
4 DTD
5 LiFSI
6 LiPO2F2

File diff suppressed because it is too large Load Diff

View File

@@ -2,330 +2,128 @@
""" """
配置文件 - 包含所有配置信息和映射关系 配置文件 - 包含所有配置信息和映射关系
""" """
import os
# ==================== API 基础配置 ==================== # API配置
# BioyondCellWorkstation 默认配置(包含所有必需参数)
API_CONFIG = { API_CONFIG = {
# API 连接配置 "api_key": "",
# "api_host": os.getenv("BIOYOND_API_HOST", "http://172.16.1.143:44389"),#实机 "api_host": ""
"api_host": os.getenv("BIOYOND_API_HOST", "http://172.16.7.149:44388"),# 仿真机 }
"api_key": os.getenv("BIOYOND_API_KEY", "8A819E5C"),
"timeout": int(os.getenv("BIOYOND_TIMEOUT", "30")), # 站点类型配置
STATION_TYPES = {
# 报送配置 "REACTION": "reaction_station", # 仅反应站
"report_token": os.getenv("BIOYOND_REPORT_TOKEN", "CHANGE_ME_TOKEN"), "DISPENSING": "dispensing_station", # 仅配液站
"HYBRID": "hybrid_station" # 混合模式
# HTTP 服务配置 }
"HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.16.2.140"), # HTTP服务监听地址监听计算机飞连ip地址
"HTTP_port": int(os.getenv("BIOYOND_HTTP_PORT", "8080")), # 默认站点配置
"debug_mode": False,# 调试模式 DEFAULT_STATION_CONFIG = {
"station_type": STATION_TYPES["REACTION"], # 默认反应站模式
"enable_reaction_station": True, # 是否启用反应站功能
"enable_dispensing_station": False, # 是否启用配液站功能
"station_name": "BioyondReactionStation", # 站点名称
"description": "Bioyond反应工作站" # 站点描述
}
# 工作流映射配置
WORKFLOW_MAPPINGS = {
"reactor_taken_out": "",
"reactor_taken_in": "",
"Solid_feeding_vials": "",
"Liquid_feeding_vials(non-titration)": "",
"Liquid_feeding_solvents": "",
"Liquid_feeding(titration)": "",
"liquid_feeding_beaker": "",
"Drip_back": "",
}
# 工作流名称到DisplaySectionName的映射
WORKFLOW_TO_SECTION_MAP = {
'reactor_taken_in': '反应器放入',
'liquid_feeding_beaker': '液体投料-烧杯',
'Liquid_feeding_vials(non-titration)': '液体投料-小瓶(非滴定)',
'Liquid_feeding_solvents': '液体投料-溶剂',
'Solid_feeding_vials': '固体投料-小瓶',
'Liquid_feeding(titration)': '液体投料-滴定',
'reactor_taken_out': '反应器取出'
} }
# 库位映射配置 # 库位映射配置
WAREHOUSE_MAPPING = { LOCATION_MAPPING = {
"粉末加样头堆栈": { 'A01': '',
"uuid": "", 'A02': '',
"site_uuids": { 'A03': '',
"A01": "3a19da56-1379-ff7c-1745-07e200b44ce2", 'A04': '',
"B01": "3a19da56-1379-2424-d751-fe6e94cef938", 'A05': '',
"C01": "3a19da56-1379-271c-03e3-6bdb590e395e", 'A06': '',
"D01": "3a19da56-1379-277f-2b1b-0d11f7cf92c6", 'A07': '',
"E01": "3a19da56-1379-2f1c-a15b-e01db90eb39a", 'A08': '',
"F01": "3a19da56-1379-3fa1-846b-088158ac0b3d", 'B01': '',
"G01": "3a19da56-1379-5aeb-d0cd-d3b4609d66e1", 'B02': '',
"H01": "3a19da56-1379-6077-8258-bdc036870b78", 'B03': '',
"I01": "3a19da56-1379-863b-a120-f606baf04617", 'B04': '',
"J01": "3a19da56-1379-8a74-74e5-35a9b41d4fd5", 'B05': '',
"K01": "3a19da56-1379-b270-b7af-f18773918abe", 'B06': '',
"L01": "3a19da56-1379-ba54-6d78-fd770a671ffc", 'B07': '',
"M01": "3a19da56-1379-c22d-c96f-0ceb5eb54a04", 'B08': '',
"N01": "3a19da56-1379-d64e-c6c5-c72ea4829888", 'C01': '',
"O01": "3a19da56-1379-d887-1a3c-6f9cce90f90e", 'C02': '',
"P01": "3a19da56-1379-e77d-0e65-7463b238a3b9", 'C03': '',
"Q01": "3a19da56-1379-edf6-1472-802ddb628774", 'C04': '',
"R01": "3a19da56-1379-f281-0273-e0ef78f0fd97", 'C05': '',
"S01": "3a19da56-1379-f924-7f68-df1fa51489f4", 'C06': '',
"T01": "3a19da56-1379-ff7c-1745-07e200b44ce2" 'C07': '',
} 'C08': '',
}, 'D01': '',
"配液站内试剂仓库": { 'D02': '',
"uuid": "", 'D03': '',
"site_uuids": { 'D04': '',
"A01": "3a19da43-57b5-294f-d663-154a1cc32270", 'D05': '',
"B01": "3a19da43-57b5-7394-5f49-54efe2c9bef2", 'D06': '',
"C01": "3a19da43-57b5-5e75-552f-8dbd0ad1075f", 'D07': '',
"A02": "3a19da43-57b5-8441-db94-b4d3875a4b6c", 'D08': '',
"B02": "3a19da43-57b5-3e41-c181-5119dddaf50c",
"C02": "3a19da43-57b5-269b-282d-fba61fe8ce96",
"A03": "3a19da43-57b5-7c1e-d02e-c40e8c33f8a1",
"B03": "3a19da43-57b5-659f-621f-1dcf3f640363",
"C03": "3a19da43-57b5-855a-6e71-f398e376dee1",
}
},
"试剂替换仓库": {
"uuid": "",
"site_uuids": {
"A01": "3a19da51-8f4e-30f3-ea08-4f8498e9b097",
"B01": "3a19da51-8f4e-1da7-beb0-80a4a01e67a8",
"C01": "3a19da51-8f4e-337d-2675-bfac46880b06",
"D01": "3a19da51-8f4e-e514-b92c-9c44dc5e489d",
"E01": "3a19da51-8f4e-22d1-dd5b-9774ddc80402",
"F01": "3a19da51-8f4e-273a-4871-dff41c29bfd9",
"G01": "3a19da51-8f4e-b32f-454f-74bc1a665653",
"H01": "3a19da51-8f4e-8c93-68c9-0b4382320f59",
"I01": "3a19da51-8f4e-360c-0149-291b47c6089b",
"J01": "3a19da51-8f4e-4152-9bca-8d64df8c1af0"
}
},
"自动堆栈-左": {
"uuid": "",
"site_uuids": {
"A01": "3a19debc-84b5-4c1c-d3a1-26830cf273ff",
"A02": "3a19debc-84b5-033b-b31f-6b87f7c2bf52",
"B01": "3a19debc-84b5-3924-172f-719ab01b125c",
"B02": "3a19debc-84b5-aad8-70c6-b8c6bb2d8750"
}
},
"自动堆栈-右": {
"uuid": "",
"site_uuids": {
"A01": "3a19debe-5200-7df2-1dd9-7d202f158864",
"A02": "3a19debe-5200-573b-6120-8b51f50e1e50",
"B01": "3a19debe-5200-7cd8-7666-851b0a97e309",
"B02": "3a19debe-5200-e6d3-96a3-baa6e3d5e484"
}
},
"手动堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea",
"A02": "3a19deae-2c7a-dc6d-c41e-ef285d946cfe",
"A03": "3a19deae-2c7a-5876-c454-6b7e224ca927",
"B01": "3a19deae-2c7a-2426-6d71-e9de3cb158b1",
"B02": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3",
"B03": "3a19deae-2c7a-b9eb-f4e3-e308e0cf839a",
"C01": "3a19deae-2c7a-32bc-768e-556647e292f3",
"C02": "3a19deae-2c7a-e97a-8484-f5a4599447c4",
"C03": "3a19deae-2c7a-3056-6504-10dc73fbc276",
"D01": "3a19deae-2c7a-ffad-875e-8c4cda61d440",
"D02": "3a19deae-2c7a-61be-601c-b6fb5610499a",
"D03": "3a19deae-2c7a-c0f7-05a7-e3fe2491e560",
"E01": "3a19deae-2c7a-a6f4-edd1-b436a7576363",
"E02": "3a19deae-2c7a-4367-96dd-1ca2186f4910",
"E03": "3a19deae-2c7a-b163-2219-23df15200311",
"F01": "3a19deae-2c7a-d594-fd6a-0d20de3c7c4a",
"F02": "3a19deae-2c7a-a194-ea63-8b342b8d8679",
"F03": "3a19deae-2c7a-f7c4-12bd-425799425698",
"G01": "3a19deae-2c7a-0b56-72f1-8ab86e53b955",
"G02": "3a19deae-2c7a-204e-95ed-1f1950f28343",
"G03": "3a19deae-2c7a-392b-62f1-4907c66343f8",
"H01": "3a19deae-2c7a-5602-e876-d27aca4e3201",
"H02": "3a19deae-2c7a-f15c-70e0-25b58a8c9702",
"H03": "3a19deae-2c7a-780b-8965-2e1345f7e834",
"I01": "3a19deae-2c7a-8849-e172-07de14ede928",
"I02": "3a19deae-2c7a-4772-a37f-ff99270bafc0",
"I03": "3a19deae-2c7a-cce7-6e4a-25ea4a2068c4",
"J01": "3a19deae-2c7a-1848-de92-b5d5ed054cc6",
"J02": "3a19deae-2c7a-1d45-b4f8-6f866530e205",
"J03": "3a19deae-2c7a-f237-89d9-8fe19025dee9"
}
},
"4号手套箱内部堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a1baa20-a7b1-c665-8b9c-d8099d07d2f6",
"A02": "3a1baa20-a7b1-93a7-c988-f9c8ad6c58c9",
"A03": "3a1baa20-a7b1-00ee-f751-da9b20b6c464",
"A04": "3a1baa20-a7b1-4712-c37b-0b5b658ef7b9",
"B01": "3a1baa20-a7b1-9847-fc9c-96d604cd1a8e",
"B02": "3a1baa20-a7b1-4ae9-e604-0601db06249c",
"B03": "3a1baa20-a7b1-8329-ea75-81ca559d9ce1",
"B04": "3a1baa20-a7b1-89c5-d96f-36e98a8f7268",
"C01": "3a1baa20-a7b1-32ec-39e6-8044733839d6",
"C02": "3a1baa20-a7b1-b573-e426-4c86040348b2",
"C03": "3a1baa20-a7b1-cca7-781e-0522b729bf5d",
"C04": "3a1baa20-a7b1-7c98-5fd9-5855355ae4b3"
}
},
"大分液瓶堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a19da3d-4f3d-bcac-2932-7542041e10e0",
"A02": "3a19da3d-4f3d-4d75-38ac-fb58ad0687c3",
"A03": "3a19da3d-4f3d-b25e-f2b1-85342a5b7eae",
"B01": "3a19da3d-4f3d-fd3e-058a-2733a0925767",
"B02": "3a19da3d-4f3d-37bd-a944-c391ad56857f",
"B03": "3a19da3d-4f3d-e353-7862-c6d1d4bc667f",
"C01": "3a19da3d-4f3d-9519-5da7-76179c958e70",
"C02": "3a19da3d-4f3d-b586-d7ed-9ec244f6f937",
"C03": "3a19da3d-4f3d-5061-249b-35dfef732811"
}
},
"小分液瓶堆栈": {
"uuid": "",
"site_uuids": {
"C03": "3a19da40-55bf-8943-d20d-a8b3ea0d16c0"
}
},
"站内Tip头盒堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a19deab-d5cc-be1e-5c37-4e9e5a664388",
"A02": "3a19deab-d5cc-b394-8141-27cb3853e8ea",
"B01": "3a19deab-d5cc-4dca-596e-ca7414d5f501",
"B02": "3a19deab-d5cc-9bc0-442b-12d9d59aa62a",
"C01": "3a19deab-d5cc-2eaf-b6a4-f0d54e4f1246",
"C02": "3a19deab-d5cc-d9f4-25df-b8018c372bc7"
}
},
"配液站内配液大板仓库(无需提前上料)": {
"uuid": "",
"site_uuids": {
"A01": "3a1a21dc-06af-3915-9cb9-80a9dc42f386"
}
},
"配液站内配液小板仓库(无需以前入料)": {
"uuid": "",
"site_uuids": {
"A01": "3a1a21de-8e8b-7938-2d06-858b36c10e31"
}
},
"移液站内大瓶板仓库(无需提前如料)": {
"uuid": "",
"site_uuids": {
"A01": "3a1a224c-c727-fa62-1f2b-0037a84b9fca"
}
},
"移液站内小瓶板仓库(无需提前入料)": {
"uuid": "",
"site_uuids": {
"A01": "3a1a224d-ed49-710c-a9c3-3fc61d479cbb"
}
},
"适配器位仓库": {
"uuid": "",
"site_uuids": {
"A01": "3a1abd46-18fe-1f56-6ced-a1f7fe08e36c"
}
},
"1号2号手套箱交接堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a1baa49-7f77-35aa-60b1-e55a45d065fa"
}
},
"2号手套箱内部堆栈": {
"uuid": "",
"site_uuids": {
"A01": "3a1baa4b-393e-9f86-3921-7a18b0a8e371",
"A02": "3a1baa4b-393e-9425-928b-ee0f6f679d44",
"A03": "3a1baa4b-393e-0baf-632b-59dfdc931a3a",
"B01": "3a1baa4b-393e-f8aa-c8a9-956f3132f05c",
"B02": "3a1baa4b-393e-ef05-42f6-53f4c6e89d70",
"B03": "3a1baa4b-393e-c07b-a924-a9f0dfda9711",
"C01": "3a1baa4b-393e-4c2b-821a-16a7fe025c48",
"C02": "3a1baa4b-393e-2eaf-61a1-9063c832d5a2",
"C03": "3a1baa4b-393e-034e-8e28-8626d934a85f"
}
}
} }
# 物料类型配置 # 物料类型配置
MATERIAL_TYPE_IDS = {
"样品板": "",
"样品": "",
"烧杯": ""
}
MATERIAL_TYPE_MAPPINGS = { MATERIAL_TYPE_MAPPINGS = {
"100ml液体": ("YB_100ml_yeti", "d37166b3-ecaa-481e-bd84-3032b795ba07"), "烧杯": "BIOYOND_PolymerStation_1FlaskCarrier",
"": ("YB_ye", "3a190ca1-2add-2b23-f8e1-bbd348b7f790"), "试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier",
"高粘液": ("YB_gaonianye", "abe8df30-563d-43d2-85e0-cabec59ddc16"), "样品板": "BIOYOND_PolymerStation_6VialCarrier",
"加样头(大)": ("YB_jia_yang_tou_da", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "加样头(大)板": ("YB_jia_yang_tou_da", "a8e714ae-2a4e-4eb9-9614-e4c140ec3f16"),
"5ml分液瓶板": ("YB_5ml_fenyepingban", "3a192fa4-007d-ec7b-456e-2a8be7a13f23"),
"5ml分液瓶": ("YB_5ml_fenyeping", "3a192c2a-ebb7-58a1-480d-8b3863bf74f4"),
"20ml分液瓶板": ("YB_20ml_fenyepingban", "3a192fa4-47db-3449-162a-eaf8aba57e27"),
"20ml分液瓶": ("YB_20ml_fenyeping", "3a192c2b-19e8-f0a3-035e-041ca8ca1035"),
"配液瓶(小)板": ("YB_peiyepingxiaoban", "3a190c8b-3284-af78-d29f-9a69463ad047"),
"配液瓶(小)": ("YB_pei_ye_xiao_Bottle", "3a190c8c-fe8f-bf48-0dc3-97afc7f508eb"),
"配液瓶(大)板": ("YB_peiyepingdaban", "53e50377-32dc-4781-b3c0-5ce45bc7dc27"),
"配液瓶(大)": ("YB_pei_ye_da_Bottle", "19c52ad1-51c5-494f-8854-576f4ca9c6ca"),
"适配器块": ("YB_shi_pei_qi_kuai", "efc3bb32-d504-4890-91c0-b64ed3ac80cf"),
"枪头盒": ("YB_qiang_tou_he", "3a192c2e-20f3-a44a-0334-c8301839d0b3"),
"枪头": ("YB_qiang_tou", "b6196971-1050-46da-9927-333e8dea062d"),
} }
SOLID_LIQUID_MAPPINGS = { # 步骤参数配置各工作流的步骤UUID
# 固体 WORKFLOW_STEP_IDS = {
"LiDFOB": { "reactor_taken_in": {
"typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469", "config": ""
"code": "",
"barCode": "",
"name": "LiDFOB",
"unit": "g",
"parameters": "",
"quantity": "2",
"warningQuantity": "1",
"details": []
}, },
# "LiPF6": { "liquid_feeding_beaker": {
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469", "liquid": "",
# "code": "", "observe": ""
# "barCode": "", },
# "name": "LiPF6", "liquid_feeding_vials_non_titration": {
# "unit": "g", "liquid": "",
# "parameters": "", "observe": ""
# "quantity": 2, },
# "warningQuantity": 1, "liquid_feeding_solvents": {
# "details": [] "liquid": "",
# }, "observe": ""
# "LiFSI": { },
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469", "solid_feeding_vials": {
# "code": "", "feeding": "",
# "barCode": "", "observe": ""
# "name": "LiFSI", },
# "unit": "g", "liquid_feeding_titration": {
# "parameters": "", "liquid": "",
# "quantity": 2, "observe": ""
# "warningQuantity": 1, },
# "details": [] "drip_back": {
# }, "liquid": "",
# "DTC": { "observe": ""
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469", }
# "code": "",
# "barCode": "",
# "name": "DTC",
# "unit": "g",
# "parameters": "",
# "quantity": 2,
# "warningQuantity": 1,
# "details": []
# },
# "LiPO2F2": {
# "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469",
# "code": "",
# "barCode": "",
# "name": "LiPO2F2",
# "unit": "g",
# "parameters": "",
# "quantity": 2,
# "warningQuantity": 1,
# "details": []
# },
# 液体
# "SA": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "EC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "VC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "AND": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "HTCN": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "DENE": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "TMSP": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "TMSB": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "EP": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "DEC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "EMC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "SN": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "DMC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
# "FEC": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"),
} }
WORKFLOW_MAPPINGS = {}
LOCATION_MAPPING = {}

View File

@@ -1,205 +1,203 @@
# experiment_workflow.py
""" """
实验流程主程序 实验流程主程序
""" """
import json import json
from unilabos.devices.workstation.bioyond_studio.reaction_station import BioyondReactionStation from bioyond_rpc import BioyondV1RPC
from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG, WORKFLOW_MAPPINGS, DECK_CONFIG, MATERIAL_TYPE_MAPPINGS from config import API_CONFIG, WORKFLOW_MAPPINGS
def run_experiment(): def run_experiment():
"""运行实验流程""" """运行实验流程"""
# 初始化Bioyond客户端 # 初始化Bioyond客户端
config = { config = {
**API_CONFIG, **API_CONFIG,
"workflow_mappings": WORKFLOW_MAPPINGS, "workflow_mappings": WORKFLOW_MAPPINGS
"material_type_mappings": MATERIAL_TYPE_MAPPINGS
} }
# 创建BioyondReactionStation实例传入deck配置 Bioyond = BioyondV1RPC(config)
Bioyond = BioyondReactionStation(
config=config,
deck=DECK_CONFIG
)
print("\n============= 多工作流参数测试(简化接口+材料缓存)=============") print("\n============= 多工作流参数测试(简化接口+材料缓存)=============")
# 显示可用的材料名称前20个 # 显示可用的材料名称前20个
available_materials = Bioyond.hardware_interface.get_available_materials() available_materials = Bioyond.get_available_materials()
print(f"可用材料名称前20个: {available_materials[:20]}") print(f"可用材料名称前20个: {available_materials[:20]}")
print(f"总共有 {len(available_materials)} 个材料可用\n") print(f"总共有 {len(available_materials)} 个材料可用\n")
# 1. 反应器放入 # 1. 反应器放入
print("1. 添加反应器放入工作流,带参数...") print("1. 添加反应器放入工作流,带参数...")
Bioyond.reactor_taken_in( Bioyond.reactor_taken_in(
assign_material_name="BTDA-DD", assign_material_name="BTDA-DD",
cutoff="10000", cutoff="10000",
temperature="-10" temperature="-10"
) )
# 2. 液体投料-烧杯 (第一个) # 2. 液体投料-烧杯 (第一个)
print("2. 添加液体投料-烧杯,带参数...") print("2. 添加液体投料-烧杯,带参数...")
Bioyond.liquid_feeding_beaker( Bioyond.liquid_feeding_beaker(
volume="34768.7", volume="34768.7",
assign_material_name="ODA", assign_material_name="ODA",
time="0", time="0",
torque_variation="1", torque_variation="1",
titration_type="1", titrationType="1",
temperature=-10 temperature=-10
) )
# 3. 液体投料-烧杯 (第二个) # 3. 液体投料-烧杯 (第二个)
print("3. 添加液体投料-烧杯,带参数...") print("3. 添加液体投料-烧杯,带参数...")
Bioyond.liquid_feeding_beaker( Bioyond.liquid_feeding_beaker(
volume="34080.9", volume="34080.9",
assign_material_name="MPDA", assign_material_name="MPDA",
time="5", time="5",
torque_variation="2", torque_variation="2",
titration_type="1", titrationType="1",
temperature=0 temperature=0
) )
# 4. 液体投料-小瓶非滴定 # 4. 液体投料-小瓶非滴定
print("4. 添加液体投料-小瓶非滴定,带参数...") print("4. 添加液体投料-小瓶非滴定,带参数...")
Bioyond.liquid_feeding_vials_non_titration( Bioyond.liquid_feeding_vials_non_titration(
volume_formula="639.5", volumeFormula="639.5",
assign_material_name="SIDA", assign_material_name="SIDA",
titration_type="1", titration_type="1",
time="0", time="0",
torque_variation="1", torque_variation="1",
temperature=-10 temperature=-10
) )
# 5. 液体投料溶剂 # 5. 液体投料溶剂
print("5. 添加液体投料溶剂,带参数...") print("5. 添加液体投料溶剂,带参数...")
Bioyond.liquid_feeding_solvents( Bioyond.liquid_feeding_solvents(
assign_material_name="NMP", assign_material_name="NMP",
volume="19000", volume="19000",
titration_type="1", titration_type="1",
time="5", time="5",
torque_variation="2", torque_variation="2",
temperature=-10 temperature=-10
) )
# 6-8. 固体进料小瓶 (三个) # 6-8. 固体进料小瓶 (三个)
print("6. 添加固体进料小瓶,带参数...") print("6. 添加固体进料小瓶,带参数...")
Bioyond.solid_feeding_vials( Bioyond.solid_feeding_vials(
material_id="3", material_id="3",
time="180", time="180",
torque_variation="2", torque_variation="2",
assign_material_name="BTDA1", assign_material_name="BTDA-1",
temperature=-10.00 temperature=-10.00
) )
print("7. 添加固体进料小瓶,带参数...") print("7. 添加固体进料小瓶,带参数...")
Bioyond.solid_feeding_vials( Bioyond.solid_feeding_vials(
material_id="3", material_id="3",
time="180", time="180",
torque_variation="2", torque_variation="2",
assign_material_name="BTDA2", assign_material_name="BTDA-2",
temperature=25.00 temperature=25.00
) )
print("8. 添加固体进料小瓶,带参数...") print("8. 添加固体进料小瓶,带参数...")
Bioyond.solid_feeding_vials( Bioyond.solid_feeding_vials(
material_id="3", material_id="3",
time="480", time="480",
torque_variation="2", torque_variation="2",
assign_material_name="BTDA3", assign_material_name="BTDA-3",
temperature=25.00 temperature=25.00
) )
# 液体投料滴定(第一个) # 液体投料滴定(第一个)
print("9. 添加液体投料滴定,带参数...") # ODPA print("9. 添加液体投料滴定,带参数...") # ODPA
Bioyond.liquid_feeding_titration( Bioyond.liquid_feeding_titration(
volume_formula="{{6-0-5}}+{{7-0-5}}+{{8-0-5}}", volume_formula="1000",
assign_material_name="BTDA-DD", assign_material_name="BTDA-DD",
titration_type="1", titration_type="1",
time="360", time="360",
torque_variation="2", torque_variation="2",
temperature="25.00" temperature="25.00"
) )
# 液体投料滴定(第二个) # 液体投料滴定(第二个)
print("10. 添加液体投料滴定,带参数...") # ODPA print("10. 添加液体投料滴定,带参数...") # ODPA
Bioyond.liquid_feeding_titration( Bioyond.liquid_feeding_titration(
volume_formula="500", volume_formula="500",
assign_material_name="BTDA-DD", assign_material_name="BTDA-DD",
titration_type="1", titration_type="1",
time="360", time="360",
torque_variation="2", torque_variation="2",
temperature="25.00" temperature="25.00"
) )
# 液体投料滴定(第三个) # 液体投料滴定(第三个)
print("11. 添加液体投料滴定,带参数...") # ODPA print("11. 添加液体投料滴定,带参数...") # ODPA
Bioyond.liquid_feeding_titration( Bioyond.liquid_feeding_titration(
volume_formula="500", volume_formula="500",
assign_material_name="BTDA-DD", assign_material_name="BTDA-DD",
titration_type="1", titration_type="1",
time="360", time="360",
torque_variation="2", torque_variation="2",
temperature="25.00" temperature="25.00"
) )
print("12. 添加液体投料滴定,带参数...") # ODPA print("12. 添加液体投料滴定,带参数...") # ODPA
Bioyond.liquid_feeding_titration( Bioyond.liquid_feeding_titration(
volume_formula="500", volume_formula="500",
assign_material_name="BTDA-DD", assign_material_name="BTDA-DD",
titration_type="1", titration_type="1",
time="360", time="360",
torque_variation="2", torque_variation="2",
temperature="25.00" temperature="25.00"
) )
print("13. 添加液体投料滴定,带参数...") # ODPA print("13. 添加液体投料滴定,带参数...") # ODPA
Bioyond.liquid_feeding_titration( Bioyond.liquid_feeding_titration(
volume_formula="500", volume_formula="500",
assign_material_name="BTDA-DD", assign_material_name="BTDA-DD",
titration_type="1", titration_type="1",
time="360", time="360",
torque_variation="2", torque_variation="2",
temperature="25.00"
)
print("14. 添加液体投料滴定,带参数...") # ODPA
Bioyond.liquid_feeding_titration(
volume_formula="500",
assign_material_name="BTDA-DD",
titration_type="1",
time="360",
torque_variation="2",
temperature="25.00" temperature="25.00"
) )
print("14. 添加液体投料滴定,带参数...") # ODPA
Bioyond.liquid_feeding_titration(
volume_formula="500",
assign_material_name="BTDA-DD",
titration_type="1",
time="360",
torque_variation="2",
temperature="25.00"
)
print("15. 添加液体投料溶剂,带参数...") print("15. 添加液体投料溶剂,带参数...")
Bioyond.liquid_feeding_solvents( Bioyond.liquid_feeding_solvents(
assign_material_name="PGME", assign_material_name="PGME",
volume="16894.6", volume="16894.6",
titration_type="1", titration_type="1",
time="360", time="360",
torque_variation="2", torque_variation="2",
temperature=25.00 temperature=25.00
) )
# 16. 反应器取出 # 16. 反应器取出
print("16. 添加反应器取出工作流...") print("16. 添加反应器取出工作流...")
Bioyond.reactor_taken_out() Bioyond.reactor_taken_out()
# 显示当前工作流序列 # 显示当前工作流序列
sequence = Bioyond.get_workflow_sequence() sequence = Bioyond.get_workflow_sequence()
print("\n当前工作流执行顺序:") print("\n当前工作流执行顺序:")
print(sequence) print(sequence)
# 执行process_and_execute_workflow合并工作流并创建任务 # 执行process_and_execute_workflow合并工作流并创建任务
print("\n4. 执行process_and_execute_workflow...") print("\n4. 执行process_and_execute_workflow...")
result = Bioyond.process_and_execute_workflow( result = Bioyond.process_and_execute_workflow(
workflow_name="test3", workflow_name="test3_86",
task_name="实验3" task_name="实验3_86"
) )
# 显示执行结果 # 显示执行结果
print("\n5. 执行结果:") print("\n5. 执行结果:")
if isinstance(result, str): if isinstance(result, str):
@@ -207,9 +205,9 @@ def run_experiment():
result_dict = json.loads(result) result_dict = json.loads(result)
if result_dict.get("success"): if result_dict.get("success"):
print("任务创建成功!") print("任务创建成功!")
# print(f"- 工作流: {result_dict.get('workflow', {}).get('name')}") print(f"- 工作流: {result_dict.get('workflow', {}).get('name')}")
# print(f"- 工作流ID: {result_dict.get('workflow', {}).get('id')}") print(f"- 工作流ID: {result_dict.get('workflow', {}).get('id')}")
# print(f"- 任务结果: {result_dict.get('task')}") print(f"- 任务结果: {result_dict.get('task')}")
else: else:
print(f"任务创建失败: {result_dict.get('error')}") print(f"任务创建失败: {result_dict.get('error')}")
except: except:
@@ -222,179 +220,179 @@ def run_experiment():
print(f"- 任务结果: {result.get('task')}") print(f"- 任务结果: {result.get('task')}")
else: else:
print(f"任务创建失败: {result.get('error')}") print(f"任务创建失败: {result.get('error')}")
# 可选:启动调度器 # 可选:启动调度器
# Bioyond.scheduler_start() # Bioyond.scheduler_start()
return Bioyond return Bioyond
# def prepare_materials(bioyond): def prepare_materials(bioyond):
# """准备实验材料(可选)""" """准备实验材料(可选)"""
# # 样品板材料数据定义 # 样品板材料数据定义
# material_data_yp_1 = { material_data_yp_1 = {
# "typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e", "typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e",
# "name": "样品板-1", "name": "样品板-1",
# "unit": "个", "unit": "",
# "quantity": 1, "quantity": 1,
# "details": [ "details": [
# { {
# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
# "name": "BPDA-DD-1", "name": "BPDA-DD-1",
# "quantity": 1, "quantity": 1,
# "x": 1, "x": 1,
# "y": 1, "y": 1,
# "Parameters": "{\"molecular\": 1}" "Parameters": "{\"molecular\": 1}"
# }, },
# { {
# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
# "name": "PEPA", "name": "PEPA",
# "quantity": 1, "quantity": 1,
# "x": 1, "x": 1,
# "y": 2, "y": 2,
# "Parameters": "{\"molecular\": 1}" "Parameters": "{\"molecular\": 1}"
# }, },
# { {
# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
# "name": "BPDA-DD-2", "name": "BPDA-DD-2",
# "quantity": 1, "quantity": 1,
# "x": 1, "x": 1,
# "y": 3, "y": 3,
# "Parameters": "{\"molecular\": 1}" "Parameters": "{\"molecular\": 1}"
# }, },
# { {
# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
# "name": "BPDA-1", "name": "BPDA-1",
# "quantity": 1, "quantity": 1,
# "x": 2, "x": 2,
# "y": 1, "y": 1,
# "Parameters": "{\"molecular\": 1}" "Parameters": "{\"molecular\": 1}"
# }, },
# { {
# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
# "name": "PMDA", "name": "PMDA",
# "quantity": 1, "quantity": 1,
# "x": 2, "x": 2,
# "y": 2, "y": 2,
# "Parameters": "{\"molecular\": 1}" "Parameters": "{\"molecular\": 1}"
# }, },
# { {
# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
# "name": "BPDA-2", "name": "BPDA-2",
# "quantity": 1, "quantity": 1,
# "x": 2, "x": 2,
# "y": 3, "y": 3,
# "Parameters": "{\"molecular\": 1}" "Parameters": "{\"molecular\": 1}"
# } }
# ], ],
# "Parameters": "{}" "Parameters": "{}"
# } }
# material_data_yp_2 = { material_data_yp_2 = {
# "typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e", "typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e",
# "name": "样品板-2", "name": "样品板-2",
# "unit": "个", "unit": "",
# "quantity": 1, "quantity": 1,
# "details": [ "details": [
# { {
# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
# "name": "BPDA-DD", "name": "BPDA-DD",
# "quantity": 1, "quantity": 1,
# "x": 1, "x": 1,
# "y": 1, "y": 1,
# "Parameters": "{\"molecular\": 1}" "Parameters": "{\"molecular\": 1}"
# }, },
# { {
# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
# "name": "SIDA", "name": "SIDA",
# "quantity": 1, "quantity": 1,
# "x": 1, "x": 1,
# "y": 2, "y": 2,
# "Parameters": "{\"molecular\": 1}" "Parameters": "{\"molecular\": 1}"
# }, },
# { {
# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
# "name": "BTDA-1", "name": "BTDA-1",
# "quantity": 1, "quantity": 1,
# "x": 2, "x": 2,
# "y": 1, "y": 1,
# "Parameters": "{\"molecular\": 1}" "Parameters": "{\"molecular\": 1}"
# }, },
# { {
# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
# "name": "BTDA-2", "name": "BTDA-2",
# "quantity": 1, "quantity": 1,
# "x": 2, "x": 2,
# "y": 2, "y": 2,
# "Parameters": "{\"molecular\": 1}" "Parameters": "{\"molecular\": 1}"
# }, },
# { {
# "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64",
# "name": "BTDA-3", "name": "BTDA-3",
# "quantity": 1, "quantity": 1,
# "x": 2, "x": 2,
# "y": 3, "y": 3,
# "Parameters": "{\"molecular\": 1}" "Parameters": "{\"molecular\": 1}"
# } }
# ], ],
# "Parameters": "{}" "Parameters": "{}"
# } }
# # 烧杯材料数据定义 # 烧杯材料数据定义
# beaker_materials = [ beaker_materials = [
# { {
# "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6",
# "name": "PDA-1", "name": "PDA-1",
# "unit": "微升", "unit": "微升",
# "quantity": 1, "quantity": 1,
# "parameters": "{\"DeviceMaterialType\":\"NMP\"}" "parameters": "{\"DeviceMaterialType\":\"NMP\"}"
# }, },
# { {
# "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6",
# "name": "TFDB", "name": "TFDB",
# "unit": "微升", "unit": "微升",
# "quantity": 1, "quantity": 1,
# "parameters": "{\"DeviceMaterialType\":\"NMP\"}" "parameters": "{\"DeviceMaterialType\":\"NMP\"}"
# }, },
# { {
# "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6",
# "name": "ODA", "name": "ODA",
# "unit": "微升", "unit": "微升",
# "quantity": 1, "quantity": 1,
# "parameters": "{\"DeviceMaterialType\":\"NMP\"}" "parameters": "{\"DeviceMaterialType\":\"NMP\"}"
# }, },
# { {
# "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6",
# "name": "MPDA", "name": "MPDA",
# "unit": "微升", "unit": "微升",
# "quantity": 1, "quantity": 1,
# "parameters": "{\"DeviceMaterialType\":\"NMP\"}" "parameters": "{\"DeviceMaterialType\":\"NMP\"}"
# }, },
# { {
# "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6",
# "name": "PDA-2", "name": "PDA-2",
# "unit": "微升", "unit": "微升",
# "quantity": 1, "quantity": 1,
# "parameters": "{\"DeviceMaterialType\":\"NMP\"}" "parameters": "{\"DeviceMaterialType\":\"NMP\"}"
# } }
# ] ]
# # 如果需要可以在这里调用add_material方法添加材料 # 如果需要可以在这里调用add_material方法添加材料
# # 例如: # 例如:
# # result = bioyond.add_material(json.dumps(material_data_yp_1)) # result = bioyond.add_material(json.dumps(material_data_yp_1))
# # print(f"添加材料结果: {result}") # print(f"添加材料结果: {result}")
# return { return {
# "sample_plates": [material_data_yp_1, material_data_yp_2], "sample_plates": [material_data_yp_1, material_data_yp_2],
# "beakers": beaker_materials "beakers": beaker_materials
# } }
if __name__ == "__main__": if __name__ == "__main__":
# 运行主实验流程 # 运行主实验流程
bioyond_client = run_experiment() bioyond_client = run_experiment()
# 可选:准备材料数据 # 可选:准备材料数据
# materials = prepare_materials(bioyond_client) # materials = prepare_materials(bioyond_client)
# print(f"\n准备的材料数据: {materials}") # print(f"\n准备的材料数据: {materials}")

View File

@@ -1,783 +0,0 @@
import json
import requests
from typing import List, Dict, Any
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
from unilabos.devices.workstation.bioyond_studio.config import (
WORKFLOW_STEP_IDS,
WORKFLOW_TO_SECTION_MAP,
ACTION_NAMES
)
from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG
class BioyondReactionStation(BioyondWorkstation):
"""Bioyond反应站类
继承自BioyondWorkstation提供反应站特定的业务方法
"""
def __init__(self, config: dict = None, deck=None, protocol_type=None, **kwargs):
"""初始化反应站
Args:
config: 配置字典应包含workflow_mappings等配置
deck: Deck对象
protocol_type: 协议类型由ROS系统传递此处忽略
**kwargs: 其他可能的参数
"""
if deck is None and config:
deck = config.get('deck')
print(f"BioyondReactionStation初始化 - config包含workflow_mappings: {'workflow_mappings' in (config or {})}")
if config and 'workflow_mappings' in config:
print(f"workflow_mappings内容: {config['workflow_mappings']}")
super().__init__(bioyond_config=config, deck=deck)
print(f"BioyondReactionStation初始化完成 - workflow_mappings: {self.workflow_mappings}")
print(f"workflow_mappings长度: {len(self.workflow_mappings)}")
# ==================== 工作流方法 ====================
def reactor_taken_out(self):
"""反应器取出"""
self.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_out"}')
reactor_taken_out_params = {"param_values": {}}
self.pending_task_params.append(reactor_taken_out_params)
print(f"成功添加反应器取出工作流")
print(f"当前队列长度: {len(self.pending_task_params)}")
return json.dumps({"suc": True})
def reactor_taken_in(
self,
assign_material_name: str,
cutoff: str = "900000",
temperature: float = -10.00
):
"""反应器放入
Args:
assign_material_name: 物料名称(不能为空)
cutoff: 粘度上限(需为有效数字字符串,默认 "900000"
temperature: 温度设定°C范围-50.00 至 100.00
Returns:
str: JSON 字符串,格式为 {"suc": True}
Raises:
ValueError: 若物料名称无效或 cutoff 格式错误
"""
if not assign_material_name:
raise ValueError("物料名称不能为空")
try:
float(cutoff)
except ValueError:
raise ValueError("cutoff 必须是有效的数字字符串")
self.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_in"}')
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
if material_id is None:
raise ValueError(f"无法找到物料 {assign_material_name} 的 ID")
if isinstance(temperature, str):
temperature = float(temperature)
step_id = WORKFLOW_STEP_IDS["reactor_taken_in"]["config"]
reactor_taken_in_params = {
"param_values": {
step_id: {
ACTION_NAMES["reactor_taken_in"]["config"]: [
{"m": 0, "n": 3, "Key": "cutoff", "Value": cutoff},
{"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id}
],
ACTION_NAMES["reactor_taken_in"]["stirring"]: [
{"m": 0, "n": 3, "Key": "temperature", "Value": f"{temperature:.2f}"}
]
}
}
}
self.pending_task_params.append(reactor_taken_in_params)
print(f"成功添加反应器放入参数: material={assign_material_name}->ID:{material_id}, cutoff={cutoff}, temp={temperature:.2f}")
print(f"当前队列长度: {len(self.pending_task_params)}")
return json.dumps({"suc": True})
def solid_feeding_vials(
self,
material_id: str,
time: str = "0",
torque_variation: int = 1,
assign_material_name: str = None,
temperature: float = 25.00
):
"""固体进料小瓶
Args:
material_id: 粉末类型ID1=盐21分钟2=面粉27分钟3=BTDA38分钟
time: 观察时间(分钟)
torque_variation: 是否观察(int类型, 1=否, 2=是)
assign_material_name: 物料名称(用于获取试剂瓶位ID)
temperature: 温度设定(°C)
"""
self.append_to_workflow_sequence('{"web_workflow_name": "Solid_feeding_vials"}')
material_id_m = self.hardware_interface._get_material_id_by_name(assign_material_name) if assign_material_name else None
if isinstance(temperature, str):
temperature = float(temperature)
feeding_step_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["feeding"]
observe_step_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["observe"]
solid_feeding_vials_params = {
"param_values": {
feeding_step_id: {
ACTION_NAMES["solid_feeding_vials"]["feeding"]: [
{"m": 0, "n": 3, "Key": "materialId", "Value": material_id},
{"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id_m} if material_id_m else {}
]
},
observe_step_id: {
ACTION_NAMES["solid_feeding_vials"]["observe"]: [
{"m": 1, "n": 0, "Key": "time", "Value": time},
{"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)},
{"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"}
]
}
}
}
self.pending_task_params.append(solid_feeding_vials_params)
print(f"成功添加固体进料小瓶参数: material_id={material_id}, time={time}min, torque={torque_variation}, temp={temperature:.2f}°C")
print(f"当前队列长度: {len(self.pending_task_params)}")
return json.dumps({"suc": True})
def liquid_feeding_vials_non_titration(
self,
volume_formula: str,
assign_material_name: str,
titration_type: str = "1",
time: str = "0",
torque_variation: int = 1,
temperature: float = 25.00
):
"""液体进料小瓶(非滴定)
Args:
volume_formula: 分液公式(μL)
assign_material_name: 物料名称
titration_type: 是否滴定(1=否, 2=是)
time: 观察时间(分钟)
torque_variation: 是否观察(int类型, 1=否, 2=是)
temperature: 温度(°C)
"""
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_vials(non-titration)"}')
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
if material_id is None:
raise ValueError(f"无法找到物料 {assign_material_name} 的 ID")
if isinstance(temperature, str):
temperature = float(temperature)
liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["liquid"]
observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["observe"]
params = {
"param_values": {
liquid_step_id: {
ACTION_NAMES["liquid_feeding_vials_non_titration"]["liquid"]: [
{"m": 0, "n": 3, "Key": "volumeFormula", "Value": volume_formula},
{"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id},
{"m": 0, "n": 3, "Key": "titrationType", "Value": titration_type}
]
},
observe_step_id: {
ACTION_NAMES["liquid_feeding_vials_non_titration"]["observe"]: [
{"m": 1, "n": 0, "Key": "time", "Value": time},
{"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)},
{"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"}
]
}
}
}
self.pending_task_params.append(params)
print(f"成功添加液体进料小瓶(非滴定)参数: volume={volume_formula}μL, material={assign_material_name}->ID:{material_id}")
print(f"当前队列长度: {len(self.pending_task_params)}")
return json.dumps({"suc": True})
def liquid_feeding_solvents(
self,
assign_material_name: str,
volume: str = None,
solvents = None,
titration_type: str = "1",
time: str = "360",
torque_variation: int = 2,
temperature: float = 25.00
):
"""液体进料-溶剂
Args:
assign_material_name: 物料名称
volume: 分液量(μL),直接指定体积(可选,如果提供solvents则自动计算)
solvents: 溶剂信息的字典或JSON字符串(可选),格式如下:
{
"additional_solvent": 33.55092503597727, # 溶剂体积(mL)
"total_liquid_volume": 48.00916988195499
}
如果提供solvents,则从中提取additional_solvent并转换为μL
titration_type: 是否滴定(1=否, 2=是)
time: 观察时间(分钟)
torque_variation: 是否观察(int类型, 1=否, 2=是)
temperature: 温度设定(°C)
"""
# 处理 volume 参数:优先使用直接传入的 volume,否则从 solvents 中提取
if volume is None and solvents is not None:
# 参数类型转换:如果是字符串则解析为字典
if isinstance(solvents, str):
try:
solvents = json.loads(solvents)
except json.JSONDecodeError as e:
raise ValueError(f"solvents参数JSON解析失败: {str(e)}")
# 参数验证
if not isinstance(solvents, dict):
raise ValueError("solvents 必须是字典类型或有效的JSON字符串")
# 提取 additional_solvent 值
additional_solvent = solvents.get("additional_solvent")
if additional_solvent is None:
raise ValueError("solvents 中没有找到 additional_solvent 字段")
# 转换为微升(μL) - 从毫升(mL)转换
volume = str(float(additional_solvent) * 1000)
elif volume is None:
raise ValueError("必须提供 volume 或 solvents 参数之一")
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_solvents"}')
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
if material_id is None:
raise ValueError(f"无法找到物料 {assign_material_name} 的 ID")
if isinstance(temperature, str):
temperature = float(temperature)
liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["liquid"]
observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["observe"]
params = {
"param_values": {
liquid_step_id: {
ACTION_NAMES["liquid_feeding_solvents"]["liquid"]: [
{"m": 0, "n": 1, "Key": "titrationType", "Value": titration_type},
{"m": 0, "n": 1, "Key": "volume", "Value": volume},
{"m": 0, "n": 1, "Key": "assignMaterialName", "Value": material_id}
]
},
observe_step_id: {
ACTION_NAMES["liquid_feeding_solvents"]["observe"]: [
{"m": 1, "n": 0, "Key": "time", "Value": time},
{"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)},
{"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"}
]
}
}
}
self.pending_task_params.append(params)
print(f"成功添加液体进料溶剂参数: material={assign_material_name}->ID:{material_id}, volume={volume}μL")
print(f"当前队列长度: {len(self.pending_task_params)}")
return json.dumps({"suc": True})
def liquid_feeding_titration(
self,
volume_formula: str,
assign_material_name: str,
titration_type: str = "1",
time: str = "90",
torque_variation: int = 2,
temperature: float = 25.00
):
"""液体进料(滴定)
Args:
volume_formula: 分液公式(μL)
assign_material_name: 物料名称
titration_type: 是否滴定(1=否, 2=是)
time: 观察时间(分钟)
torque_variation: 是否观察(int类型, 1=否, 2=是)
temperature: 温度(°C)
"""
self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}')
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
if material_id is None:
raise ValueError(f"无法找到物料 {assign_material_name} 的 ID")
if isinstance(temperature, str):
temperature = float(temperature)
liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"]
observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"]
params = {
"param_values": {
liquid_step_id: {
ACTION_NAMES["liquid_feeding_titration"]["liquid"]: [
{"m": 0, "n": 3, "Key": "volumeFormula", "Value": volume_formula},
{"m": 0, "n": 3, "Key": "titrationType", "Value": titration_type},
{"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id}
]
},
observe_step_id: {
ACTION_NAMES["liquid_feeding_titration"]["observe"]: [
{"m": 1, "n": 0, "Key": "time", "Value": time},
{"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)},
{"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"}
]
}
}
}
self.pending_task_params.append(params)
print(f"成功添加液体进料滴定参数: volume={volume_formula}μL, material={assign_material_name}->ID:{material_id}")
print(f"当前队列长度: {len(self.pending_task_params)}")
return json.dumps({"suc": True})
def liquid_feeding_beaker(
self,
volume: str = "35000",
assign_material_name: str = "BAPP",
time: str = "0",
torque_variation: int = 1,
titration_type: str = "1",
temperature: float = 25.00
):
"""液体进料烧杯
Args:
volume: 分液量(μL)
assign_material_name: 物料名称(试剂瓶位)
time: 观察时间(分钟)
torque_variation: 是否观察(int类型, 1=否, 2=是)
titration_type: 是否滴定(1=否, 2=是)
temperature: 温度设定(°C)
"""
self.append_to_workflow_sequence('{"web_workflow_name": "liquid_feeding_beaker"}')
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
if material_id is None:
raise ValueError(f"无法找到物料 {assign_material_name} 的 ID")
if isinstance(temperature, str):
temperature = float(temperature)
liquid_step_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["liquid"]
observe_step_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["observe"]
params = {
"param_values": {
liquid_step_id: {
ACTION_NAMES["liquid_feeding_beaker"]["liquid"]: [
{"m": 0, "n": 2, "Key": "volume", "Value": volume},
{"m": 0, "n": 2, "Key": "assignMaterialName", "Value": material_id},
{"m": 0, "n": 2, "Key": "titrationType", "Value": titration_type}
]
},
observe_step_id: {
ACTION_NAMES["liquid_feeding_beaker"]["observe"]: [
{"m": 1, "n": 0, "Key": "time", "Value": time},
{"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)},
{"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"}
]
}
}
}
self.pending_task_params.append(params)
print(f"成功添加液体进料烧杯参数: volume={volume}μL, material={assign_material_name}->ID:{material_id}")
print(f"当前队列长度: {len(self.pending_task_params)}")
return json.dumps({"suc": True})
def drip_back(
self,
assign_material_name: str,
volume: str,
titration_type: str = "1",
time: str = "90",
torque_variation: int = 2,
temperature: float = 25.00
):
"""滴回去
Args:
assign_material_name: 物料名称(液体种类)
volume: 分液量(μL)
titration_type: 是否滴定(1=否, 2=是)
time: 观察时间(分钟)
torque_variation: 是否观察(int类型, 1=否, 2=是)
temperature: 温度(°C)
"""
self.append_to_workflow_sequence('{"web_workflow_name": "drip_back"}')
material_id = self.hardware_interface._get_material_id_by_name(assign_material_name)
if material_id is None:
raise ValueError(f"无法找到物料 {assign_material_name} 的 ID")
if isinstance(temperature, str):
temperature = float(temperature)
liquid_step_id = WORKFLOW_STEP_IDS["drip_back"]["liquid"]
observe_step_id = WORKFLOW_STEP_IDS["drip_back"]["observe"]
params = {
"param_values": {
liquid_step_id: {
ACTION_NAMES["drip_back"]["liquid"]: [
{"m": 0, "n": 1, "Key": "titrationType", "Value": titration_type},
{"m": 0, "n": 1, "Key": "assignMaterialName", "Value": material_id},
{"m": 0, "n": 1, "Key": "volume", "Value": volume}
]
},
observe_step_id: {
ACTION_NAMES["drip_back"]["observe"]: [
{"m": 1, "n": 0, "Key": "time", "Value": time},
{"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)},
{"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"}
]
}
}
}
self.pending_task_params.append(params)
print(f"成功添加滴回去参数: material={assign_material_name}->ID:{material_id}, volume={volume}μL")
print(f"当前队列长度: {len(self.pending_task_params)}")
return json.dumps({"suc": True})
# ==================== 工作流管理方法 ====================
def get_workflow_sequence(self) -> List[str]:
"""获取当前工作流执行顺序
Returns:
工作流名称列表
"""
id_to_name = {workflow_id: name for name, workflow_id in self.workflow_mappings.items()}
workflow_names = []
for workflow_id in self.workflow_sequence:
workflow_name = id_to_name.get(workflow_id, workflow_id)
workflow_names.append(workflow_name)
print(f"工作流序列: {workflow_names}")
return workflow_names
def workflow_step_query(self, workflow_id: str) -> dict:
"""查询工作流步骤参数
Args:
workflow_id: 工作流ID
Returns:
工作流步骤参数字典
"""
return self.hardware_interface.workflow_step_query(workflow_id)
def create_order(self, json_str: str) -> dict:
"""创建订单
Args:
json_str: 订单参数的JSON字符串
Returns:
创建结果
"""
return self.hardware_interface.create_order(json_str)
# ==================== 工作流执行核心方法 ====================
def process_web_workflows(self, web_workflow_json: str) -> List[Dict[str, str]]:
"""处理网页工作流列表
Args:
web_workflow_json: JSON 格式的网页工作流列表
Returns:
List[Dict[str, str]]: 包含工作流 ID 和名称的字典列表
"""
try:
web_workflow_data = json.loads(web_workflow_json)
web_workflow_list = web_workflow_data.get("web_workflow_list", [])
workflows_result = []
for name in web_workflow_list:
workflow_id = self.workflow_mappings.get(name, "")
if not workflow_id:
print(f"警告:未找到工作流名称 {name} 对应的 ID")
continue
workflows_result.append({"id": workflow_id, "name": name})
print(f"process_web_workflows 输出: {workflows_result}")
return workflows_result
except json.JSONDecodeError as e:
print(f"错误:无法解析 web_workflow_json: {e}")
return []
except Exception as e:
print(f"错误:处理工作流失败: {e}")
return []
def process_and_execute_workflow(self, workflow_name: str, task_name: str) -> dict:
"""
一站式处理工作流程:解析网页工作流列表,合并工作流(带参数),然后发布任务
Args:
workflow_name: 合并后的工作流名称
task_name: 任务名称
Returns:
任务创建结果
"""
web_workflow_list = self.get_workflow_sequence()
print(f"\n{'='*60}")
print(f"📋 处理网页工作流列表: {web_workflow_list}")
print(f"{'='*60}")
web_workflow_json = json.dumps({"web_workflow_list": web_workflow_list})
workflows_result = self.process_web_workflows(web_workflow_json)
if not workflows_result:
return self._create_error_result("处理网页工作流列表失败", "process_web_workflows")
print(f"workflows_result 类型: {type(workflows_result)}")
print(f"workflows_result 内容: {workflows_result}")
workflows_with_params = self._build_workflows_with_parameters(workflows_result)
merge_data = {
"name": workflow_name,
"workflows": workflows_with_params
}
# print(f"\n🔄 合并工作流(带参数),名称: {workflow_name}")
merged_workflow = self.merge_workflow_with_parameters(json.dumps(merge_data))
if not merged_workflow:
return self._create_error_result("合并工作流失败", "merge_workflow_with_parameters")
workflow_id = merged_workflow.get("subWorkflows", [{}])[0].get("id", "")
# print(f"\n📤 使用工作流创建任务: {workflow_name} (ID: {workflow_id})")
order_params = [{
"orderCode": f"task_{self.hardware_interface.get_current_time_iso8601()}",
"orderName": task_name,
"workFlowId": workflow_id,
"borderNumber": 1,
"paramValues": {}
}]
result = self.create_order(json.dumps(order_params))
if not result:
return self._create_error_result("创建任务失败", "create_order")
# 清空工作流序列和参数,防止下次执行时累积重复
self.pending_task_params = []
self.clear_workflows() # 清空工作流序列,避免重复累积
# print(f"\n✅ 任务创建成功: {result}")
# print(f"\n✅ 任务创建成功")
print(f"{'='*60}\n")
return json.dumps({"success": True, "result": result})
def _build_workflows_with_parameters(self, workflows_result: list) -> list:
"""
构建带参数的工作流列表
Args:
workflows_result: 处理后的工作流列表(应为包含 id 和 name 的字典列表)
Returns:
符合新接口格式的工作流参数结构
"""
workflows_with_params = []
total_params = 0
successful_params = 0
failed_params = []
for idx, workflow_info in enumerate(workflows_result):
if not isinstance(workflow_info, dict):
print(f"错误workflows_result[{idx}] 不是字典,而是 {type(workflow_info)}: {workflow_info}")
continue
workflow_id = workflow_info.get("id")
if not workflow_id:
print(f"警告workflows_result[{idx}] 缺少 'id'")
continue
workflow_name = workflow_info.get("name", "")
# print(f"\n🔧 处理工作流 [{idx}]: {workflow_name} (ID: {workflow_id})")
if idx >= len(self.pending_task_params):
# print(f" ⚠️ 无对应参数,跳过")
workflows_with_params.append({"id": workflow_id})
continue
param_data = self.pending_task_params[idx]
param_values = param_data.get("param_values", {})
if not param_values:
# print(f" ⚠️ 参数为空,跳过")
workflows_with_params.append({"id": workflow_id})
continue
step_parameters = {}
for step_id, actions_dict in param_values.items():
# print(f" 📍 步骤ID: {step_id}")
for action_name, param_list in actions_dict.items():
# print(f" 🔹 模块: {action_name}, 参数数量: {len(param_list)}")
if step_id not in step_parameters:
step_parameters[step_id] = {}
if action_name not in step_parameters[step_id]:
step_parameters[step_id][action_name] = []
for param_item in param_list:
param_key = param_item.get("Key", "")
param_value = param_item.get("Value", "")
total_params += 1
step_parameters[step_id][action_name].append({
"Key": param_key,
"DisplayValue": param_value,
"Value": param_value
})
successful_params += 1
# print(f" ✓ {param_key} = {param_value}")
workflows_with_params.append({
"id": workflow_id,
"stepParameters": step_parameters
})
self._print_mapping_stats(total_params, successful_params, failed_params)
return workflows_with_params
def _print_mapping_stats(self, total: int, success: int, failed: list):
"""打印参数映射统计"""
print(f"\n{'='*20} 参数映射统计 {'='*20}")
print(f"📊 总参数数量: {total}")
print(f"✅ 成功映射: {success}")
print(f"❌ 映射失败: {len(failed)}")
if not failed:
print("🎉 成功映射所有参数!")
else:
print(f"⚠️ 失败的参数: {', '.join(failed)}")
success_rate = (success/total*100) if total > 0 else 0
print(f"📈 映射成功率: {success_rate:.1f}%")
print("="*60)
def _create_error_result(self, error_msg: str, step: str) -> str:
"""创建统一的错误返回格式"""
print(f"{error_msg}")
return json.dumps({
"success": False,
"error": f"process_and_execute_workflow: {error_msg}",
"method": "process_and_execute_workflow",
"step": step
})
def merge_workflow_with_parameters(self, json_str: str) -> dict:
"""
调用新接口:合并工作流并传递参数
Args:
json_str: JSON格式的字符串包含:
- name: 工作流名称
- workflows: [{"id": "工作流ID", "stepParameters": {...}}]
Returns:
合并后的工作流信息
"""
try:
data = json.loads(json_str)
# 在工作流名称后面添加时间戳,避免重复
if "name" in data and data["name"]:
timestamp = self.hardware_interface.get_current_time_iso8601().replace(":", "-").replace(".", "-")
original_name = data["name"]
data["name"] = f"{original_name}_{timestamp}"
print(f"🕒 工作流名称已添加时间戳: {original_name} -> {data['name']}")
request_data = {
"apiKey": API_CONFIG["api_key"],
"requestTime": self.hardware_interface.get_current_time_iso8601(),
"data": data
}
print(f"\n📤 发送合并请求:")
print(f" 工作流名称: {data.get('name')}")
print(f" 子工作流数量: {len(data.get('workflows', []))}")
# 打印完整的POST请求内容
print(f"\n🔍 POST请求详细内容:")
print(f" URL: {self.hardware_interface.host}/api/lims/workflow/merge-workflow-with-parameters")
print(f" Headers: {{'Content-Type': 'application/json'}}")
print(f" Request Data:")
print(f" {json.dumps(request_data, indent=4, ensure_ascii=False)}")
#
response = requests.post(
f"{self.hardware_interface.host}/api/lims/workflow/merge-workflow-with-parameters",
json=request_data,
headers={"Content-Type": "application/json"},
timeout=30
)
# # 打印响应详细内容
# print(f"\n📥 POST响应详细内容:")
# print(f" 状态码: {response.status_code}")
# print(f" 响应头: {dict(response.headers)}")
# print(f" 响应体: {response.text}")
# #
try:
result = response.json()
# #
# print(f"\n📋 解析后的响应JSON:")
# print(f" {json.dumps(result, indent=4, ensure_ascii=False)}")
# #
except json.JSONDecodeError:
print(f"❌ 服务器返回非 JSON 格式响应: {response.text}")
return None
if result.get("code") == 1:
print(f"✅ 工作流合并成功(带参数)")
return result.get("data", {})
else:
error_msg = result.get('message', '未知错误')
print(f"❌ 工作流合并失败: {error_msg}")
return None
except requests.exceptions.Timeout:
print(f"❌ 合并工作流请求超时")
return None
except requests.exceptions.RequestException as e:
print(f"❌ 合并工作流网络异常: {str(e)}")
return None
except json.JSONDecodeError as e:
print(f"❌ 合并工作流响应解析失败: {str(e)}")
return None
except Exception as e:
print(f"❌ 合并工作流异常: {str(e)}")
return None
def _validate_and_refresh_workflow_if_needed(self, workflow_name: str) -> bool:
"""验证工作流ID是否有效如果无效则重新合并
Args:
workflow_name: 工作流名称
Returns:
bool: 验证或刷新是否成功
"""
print(f"\n🔍 验证工作流ID有效性...")
if not self.workflow_sequence:
print(f" ⚠️ 工作流序列为空,需要重新合并")
return False
first_workflow_id = self.workflow_sequence[0]
try:
structure = self.workflow_step_query(first_workflow_id)
if structure:
print(f" ✅ 工作流ID有效")
return True
else:
print(f" ⚠️ 工作流ID已过期需要重新合并")
return False
except Exception as e:
print(f" ❌ 工作流ID验证失败: {e}")
print(f" 💡 将重新合并工作流")
return False

File diff suppressed because it is too large Load Diff

View File

@@ -24,13 +24,6 @@ class ElectrodeSheetState(TypedDict):
thickness: float # 厚度 (mm) thickness: float # 厚度 (mm)
mass: float # 质量 (g) mass: float # 质量 (g)
material_type: str # 材料类型(正极、负极、隔膜、弹片、垫片、铝箔等) material_type: str # 材料类型(正极、负极、隔膜、弹片、垫片、铝箔等)
height: float
electrolyte_name: str
data_electrolyte_code: str
open_circuit_voltage: float
assembly_pressure: float
electrolyte_volume: float
info: Optional[str] # 附加信息 info: Optional[str] # 附加信息
class ElectrodeSheet(Resource): class ElectrodeSheet(Resource):
@@ -68,7 +61,6 @@ class ElectrodeSheet(Resource):
info=None info=None
) )
# TODO: 这个还要不要给self._unilabos_state赋值的
def load_state(self, state: Dict[str, Any]) -> None: def load_state(self, state: Dict[str, Any]) -> None:
"""格式不变""" """格式不变"""
super().load_state(state) super().load_state(state)
@@ -154,10 +146,10 @@ class MaterialHole(Resource):
): ):
"""放置极片""" """放置极片"""
# TODO: 这里要改diameter找不到加入._unilabos_state后应该没问题 # TODO: 这里要改diameter找不到加入._unilabos_state后应该没问题
#if resource._unilabos_state["diameter"] > self._unilabos_state["diameter"]: if resource._unilabos_state["diameter"] > self._unilabos_state["diameter"]:
# raise ValueError(f"极片直径 {resource._unilabos_state['diameter']} 超过洞位直径 {self._unilabos_state['diameter']}") raise ValueError(f"极片直径 {resource._unilabos_state['diameter']} 超过洞位直径 {self._unilabos_state['diameter']}")
#if len(self.children) >= self._unilabos_state["max_sheets"]: if len(self.children) >= self._unilabos_state["max_sheets"]:
# raise ValueError(f"洞位已满,无法放置更多极片") raise ValueError(f"洞位已满,无法放置更多极片")
super().assign_child_resource(resource, location, reassign) super().assign_child_resource(resource, location, reassign)
# 根据children的编号取物料对象。 # 根据children的编号取物料对象。
@@ -172,6 +164,8 @@ class MaterialPlateState(TypedDict):
hole_diameter: float hole_diameter: float
info: Optional[str] # 附加信息 info: Optional[str] # 附加信息
class MaterialPlate(ItemizedResource[MaterialHole]): class MaterialPlate(ItemizedResource[MaterialHole]):
"""料板类 - 4x4个洞位每个洞位放1个极片""" """料板类 - 4x4个洞位每个洞位放1个极片"""
@@ -329,13 +323,12 @@ class PlateSlot(ResourceStack):
class ClipMagazineHole(Container): class ClipMagazineHole(Container):
"""子弹夹洞位类""" """子弹夹洞位类"""
children: List[ElectrodeSheet] = []
def __init__( def __init__(
self, self,
name: str, name: str,
diameter: float, diameter: float,
depth: float, depth: float,
max_sheets: int = 100,
category: str = "clip_magazine_hole", category: str = "clip_magazine_hole",
): ):
"""初始化子弹夹洞位 """初始化子弹夹洞位
@@ -344,7 +337,6 @@ class ClipMagazineHole(Container):
name: 洞位名称 name: 洞位名称
diameter: 洞直径 (mm) diameter: 洞直径 (mm)
depth: 洞深度 (mm) depth: 洞深度 (mm)
max_sheets: 最大极片数量
category: 类别 category: 类别
""" """
super().__init__( super().__init__(
@@ -356,36 +348,131 @@ class ClipMagazineHole(Container):
) )
self.diameter = diameter self.diameter = diameter
self.depth = depth self.depth = depth
self.max_sheets = max_sheets
self._sheets: List[ElectrodeSheet] = []
def can_add_sheet(self, sheet: ElectrodeSheet) -> bool: def can_add_sheet(self, sheet: ElectrodeSheet) -> bool:
"""检查是否可以添加极片""" """检查是否可以添加极片
return (len(self._sheets) < self.max_sheets and
sheet.diameter <= self.diameter) 根据洞的深度和极片的厚度来判断是否可以添加极片
"""
# 检查极片直径是否适合洞的直径
if sheet._unilabos_state["diameter"] > self.diameter:
return False
# 计算当前已添加极片的总厚度
current_thickness = sum(s._unilabos_state["thickness"] for s in self.children)
# 检查添加新极片后总厚度是否超过洞的深度
if current_thickness + sheet._unilabos_state["thickness"] > self.depth:
return False
return True
def add_sheet(self, sheet: ElectrodeSheet) -> None:
"""添加极片"""
if not self.can_add_sheet(sheet):
raise ValueError(f"无法向洞位 {self.name} 添加极片")
self._sheets.append(sheet)
def take_sheet(self) -> ElectrodeSheet: def assign_child_resource(
"""取出极片""" self,
if len(self._sheets) == 0: resource: ElectrodeSheet,
raise ValueError(f"洞位 {self.name} 没有极片") location: Optional[Coordinate] = None,
return self._sheets.pop() reassign: bool = True,
):
"""放置极片到洞位中
Args:
resource: 要放置的极片
location: 极片在洞位中的位置对于洞位通常为None
reassign: 是否允许重新分配
"""
# 检查是否可以添加极片
if not self.can_add_sheet(resource):
raise ValueError(f"无法向洞位 {self.name} 添加极片:直径或厚度不匹配")
# 调用父类方法实际执行分配
super().assign_child_resource(resource, location, reassign)
def unassign_child_resource(self, resource: ElectrodeSheet):
"""从洞位中移除极片
Args:
resource: 要移除的极片
"""
if resource not in self.children:
raise ValueError(f"极片 {resource.name} 不在洞位 {self.name}")
# 调用父类方法实际执行移除
super().unassign_child_resource(resource)
def get_sheet_count(self) -> int:
"""获取极片数量"""
return len(self._sheets)
def serialize_state(self) -> Dict[str, Any]: def serialize_state(self) -> Dict[str, Any]:
return { return {
"sheet_count": len(self._sheets), "sheet_count": len(self.children),
"sheets": [sheet.serialize() for sheet in self._sheets], "sheets": [sheet.serialize() for sheet in self.children],
} }
class ClipMagazine_four(ItemizedResource[ClipMagazineHole]):
"""子弹夹类 - 有4个洞位每个洞位放多个极片"""
children: List[ClipMagazineHole]
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
hole_diameter: float = 14.0,
hole_depth: float = 10.0,
hole_spacing: float = 25.0,
max_sheets_per_hole: int = 100,
category: str = "clip_magazine_four",
model: Optional[str] = None,
):
"""初始化子弹夹
Args:
name: 子弹夹名称
size_x: 长度 (mm)
size_y: 宽度 (mm)
size_z: 高度 (mm)
hole_diameter: 洞直径 (mm)
hole_depth: 洞深度 (mm)
hole_spacing: 洞位间距 (mm)
max_sheets_per_hole: 每个洞位最大极片数量
category: 类别
model: 型号
"""
# 创建4个洞位排成2x2布局
holes = create_ordered_items_2d(
klass=ClipMagazineHole,
num_items_x=2,
num_items_y=2,
dx=(size_x - 2 * hole_spacing) / 2, # 居中
dy=(size_y - hole_spacing) / 2, # 居中
dz=size_z - 0,
item_dx=hole_spacing,
item_dy=hole_spacing,
diameter=hole_diameter,
depth=hole_depth,
)
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
ordered_items=holes,
category=category,
model=model,
)
# 保存洞位的直径和深度
self.hole_diameter = hole_diameter
self.hole_depth = hole_depth
self.max_sheets_per_hole = max_sheets_per_hole
def serialize(self) -> dict:
return {
**super().serialize(),
"hole_diameter": self.hole_diameter,
"hole_depth": self.hole_depth,
"max_sheets_per_hole": self.max_sheets_per_hole,
}
# TODO: 这个要改 # TODO: 这个要改
class ClipMagazine(ItemizedResource[ClipMagazineHole]): class ClipMagazine(ItemizedResource[ClipMagazineHole]):
"""子弹夹类 - 有6个洞位每个洞位放多个极片""" """子弹夹类 - 有6个洞位每个洞位放多个极片"""
@@ -458,9 +545,9 @@ class BatteryState(TypedDict):
"""电池状态字典""" """电池状态字典"""
diameter: float diameter: float
height: float height: float
assembly_pressure: float
electrolyte_volume: float
electrolyte_name: str electrolyte_name: str
electrolyte_volume: float
class Battery(Resource): class Battery(Resource):
"""电池类 - 可容纳极片""" """电池类 - 可容纳极片"""
@@ -469,9 +556,6 @@ class Battery(Resource):
def __init__( def __init__(
self, self,
name: str, name: str,
size_x=1,
size_y=1,
size_z=1,
category: str = "battery", category: str = "battery",
): ):
"""初始化电池 """初始化电池
@@ -492,13 +576,7 @@ class Battery(Resource):
size_z=1, size_z=1,
category=category, category=category,
) )
self._unilabos_state: BatteryState = BatteryState( self._unilabos_state: BatteryState = BatteryState()
diameter = 1.0,
height = 1.0,
assembly_pressure = 1.0,
electrolyte_volume = 1.0,
electrolyte_name = "DP001"
)
def add_electrolyte_with_bottle(self, bottle: Bottle) -> bool: def add_electrolyte_with_bottle(self, bottle: Bottle) -> bool:
to_add_name = bottle._unilabos_state["electrolyte_name"] to_add_name = bottle._unilabos_state["electrolyte_name"]
@@ -586,7 +664,6 @@ class BatteryPressSlot(Resource):
reassign: bool = True, reassign: bool = True,
): ):
"""放置极片""" """放置极片"""
# TODO: 让高京看下槽位只有一个电池时是否这么写。
if self.has_battery(): if self.has_battery():
raise ValueError(f"槽位已含有一个电池,无法再放置其他电池") raise ValueError(f"槽位已含有一个电池,无法再放置其他电池")
super().assign_child_resource(resource, location, reassign) super().assign_child_resource(resource, location, reassign)
@@ -595,7 +672,6 @@ class BatteryPressSlot(Resource):
def get_battery_info(self, index: int) -> Battery: def get_battery_info(self, index: int) -> Battery:
return self.children[0] return self.children[0]
# TODO:这个移液枪架子看一下从哪继承
class TipBox64State(TypedDict): class TipBox64State(TypedDict):
"""电池状态字典""" """电池状态字典"""
tip_diameter: float = 5.0 tip_diameter: float = 5.0
@@ -654,6 +730,15 @@ class TipBox64(TipRack):
make_tip=make_tip, make_tip=make_tip,
) )
self._unilabos_state: WasteTipBoxstate = WasteTipBoxstate() self._unilabos_state: WasteTipBoxstate = WasteTipBoxstate()
# 记录网格参数用于前端渲染
self._grid_params = {
"num_items_x": 8,
"num_items_y": 8,
"dx": 8.0,
"dy": 8.0,
"item_dx": 9.0,
"item_dy": 9.0,
}
super().__init__( super().__init__(
name=name, name=name,
size_x=size_x, size_x=size_x,
@@ -665,6 +750,12 @@ class TipBox64(TipRack):
with_tips=True, with_tips=True,
) )
def serialize(self) -> dict:
return {
**super().serialize(),
**self._grid_params,
}
class WasteTipBoxstate(TypedDict): class WasteTipBoxstate(TypedDict):
@@ -742,6 +833,15 @@ class BottleRackState(TypedDict):
name_to_index: dict name_to_index: dict
class BottleRackState(TypedDict):
""" bottle_diameter: 瓶子直径 (mm)
bottle_height: 瓶子高度 (mm)
position_spacing: 位置间距 (mm)"""
bottle_diameter: float
bottle_height: float
position_spacing: float
name_to_index: dict
class BottleRack(Resource): class BottleRack(Resource):
"""瓶架类 - 12个待配位置+12个已配位置""" """瓶架类 - 12个待配位置+12个已配位置"""
@@ -861,6 +961,7 @@ class BottleRack(Resource):
"padding_y": self.padding_y, "padding_y": self.padding_y,
} }
class BottleState(TypedDict): class BottleState(TypedDict):
diameter: float diameter: float
height: float height: float
@@ -913,96 +1014,27 @@ class Bottle(Resource):
data.update(self._unilabos_state) # Container自身的信息云端物料将保存这一data本地也通过这里的data进行读写当前类用来表示这个物料的长宽高大小的属性而datastate用来表示物料的内容细节等 data.update(self._unilabos_state) # Container自身的信息云端物料将保存这一data本地也通过这里的data进行读写当前类用来表示这个物料的长宽高大小的属性而datastate用来表示物料的内容细节等
return data return data
class ClipMagazine_four(ItemizedResource[ClipMagazineHole]):
"""子弹夹类 - 有4个洞位每个洞位放多个极片"""
children: List[ClipMagazineHole]
def __init__(
self,
name: str,
size_x: float,
size_y: float,
size_z: float,
hole_diameter: float = 14.0,
hole_depth: float = 10.0,
hole_spacing: float = 25.0,
max_sheets_per_hole: int = 100,
category: str = "clip_magazine_four",
model: Optional[str] = None,
):
"""初始化子弹夹
Args:
name: 子弹夹名称
size_x: 长度 (mm)
size_y: 宽度 (mm)
size_z: 高度 (mm)
hole_diameter: 洞直径 (mm)
hole_depth: 洞深度 (mm)
hole_spacing: 洞位间距 (mm)
max_sheets_per_hole: 每个洞位最大极片数量
category: 类别
model: 型号
"""
# 创建4个洞位排成2x2布局
holes = create_ordered_items_2d(
klass=ClipMagazineHole,
num_items_x=2,
num_items_y=2,
dx=(size_x - 2 * hole_spacing) / 2, # 居中
dy=(size_y - hole_spacing) / 2, # 居中
dz=size_z - 0,
item_dx=hole_spacing,
item_dy=hole_spacing,
diameter=hole_diameter,
depth=hole_depth,
)
super().__init__(
name=name,
size_x=size_x,
size_y=size_y,
size_z=size_z,
ordered_items=holes,
category=category,
model=model,
)
# 保存洞位的直径和深度
self.hole_diameter = hole_diameter
self.hole_depth = hole_depth
self.max_sheets_per_hole = max_sheets_per_hole
def serialize(self) -> dict:
return {
**super().serialize(),
"hole_diameter": self.hole_diameter,
"hole_depth": self.hole_depth,
"max_sheets_per_hole": self.max_sheets_per_hole,
}
class CoincellDeck(Deck): class CoincellDeck(Deck):
"""纽扣电池组装工作站台面类""" """纽扣电池组装工作站台面类"""
def __init__( def __init__(
self, self,
name: str = "coin_cell_deck", name: str = "coin_cell_deck",
size_x: float = 3650.0, # 1m size_x: float = 1620.0, # 3.66m
size_y: float = 1550.0, # 1m size_y: float = 1270.0, # 1.23m
size_z: float = 2100.0, # 0.9m size_z: float = 500.0,
origin: Coordinate = Coordinate(-4000, 2000, 0), origin: Coordinate = Coordinate(0, 0, 0),
category: str = "coin_cell_deck", category: str = "coin_cell_deck",
setup: bool = False, # 是否自动执行 setup
): ):
"""初始化纽扣电池组装工作站台面 """初始化纽扣电池组装工作站台面
Args: Args:
name: 台面名称 name: 台面名称
size_x: 长度 (mm) - 1m size_x: 长度 (mm) - 3.66m
size_y: 宽度 (mm) - 1m size_y: 宽度 (mm) - 1.23m
size_z: 高度 (mm) - 0.9m size_z: 高度 (mm)
origin: 原点坐标 origin: 原点坐标
category: 类别 category: 类别
setup: 是否自动执行 setup 配置标准布局
""" """
super().__init__( super().__init__(
name=name, name=name,
@@ -1012,154 +1044,246 @@ class CoincellDeck(Deck):
origin=origin, origin=origin,
category=category, category=category,
) )
if setup:
self.setup()
def setup(self) -> None: #if __name__ == "__main__":
"""设置工作站的标准布局 - 包含子弹夹、料盘、瓶架等完整配置""" # # 转移极片的测试代码
# ====================================== 子弹夹 ============================================ # deck = CoincellDeck("coin_cell_deck")
zip_dan_jia = ClipMagazine_four("zi_dan_jia", 80, 80, 10) # ban_cao_wei = PlateSlot("ban_cao_wei", max_plates=8)
self.assign_child_resource(zip_dan_jia, Coordinate(x=1400, y=50, z=0)) # deck.assign_child_resource(ban_cao_wei, Coordinate(x=0, y=0, z=0))
zip_dan_jia2 = ClipMagazine_four("zi_dan_jia2", 80, 80, 10) #
self.assign_child_resource(zip_dan_jia2, Coordinate(x=1600, y=200, z=0)) # plate_1 = MaterialPlate("plate_1", 1,1,1, fill=True)
zip_dan_jia3 = ClipMagazine("zi_dan_jia3", 80, 80, 10) # for i, hole in enumerate(plate_1.children):
self.assign_child_resource(zip_dan_jia3, Coordinate(x=1500, y=200, z=0)) # sheet = ElectrodeSheet(f"hole_{i}_sheet_1")
zip_dan_jia4 = ClipMagazine("zi_dan_jia4", 80, 80, 10) # sheet._unilabos_state = {
self.assign_child_resource(zip_dan_jia4, Coordinate(x=1500, y=300, z=0)) # "diameter": 14,
zip_dan_jia5 = ClipMagazine("zi_dan_jia5", 80, 80, 10) # "info": "NMC",
self.assign_child_resource(zip_dan_jia5, Coordinate(x=1600, y=300, z=0)) # "mass": 5.0,
zip_dan_jia6 = ClipMagazine("zi_dan_jia6", 80, 80, 10) # "material_type": "positive_electrode",
self.assign_child_resource(zip_dan_jia6, Coordinate(x=1530, y=500, z=0)) # "thickness": 0.1
zip_dan_jia7 = ClipMagazine("zi_dan_jia7", 80, 80, 10) # }
self.assign_child_resource(zip_dan_jia7, Coordinate(x=1180, y=400, z=0)) # hole._unilabos_state = {
zip_dan_jia8 = ClipMagazine("zi_dan_jia8", 80, 80, 10) # "depth": 1.0,
self.assign_child_resource(zip_dan_jia8, Coordinate(x=1280, y=400, z=0)) # "diameter": 14,
# "info": "",
# 为子弹夹添加极片 # "max_sheets": 1
for i in range(4): # }
jipian = ElectrodeSheet(name=f"zi_dan_jia_jipian_{i}", size_x=12, size_y=12, size_z=0.1) # hole.assign_child_resource(sheet, Coordinate.zero())
zip_dan_jia2.children[i].assign_child_resource(jipian, location=None) # plate_1._unilabos_state = {
for i in range(4): # "hole_spacing_x": 20.0,
jipian2 = ElectrodeSheet(name=f"zi_dan_jia2_jipian_{i}", size_x=12, size_y=12, size_z=0.1) # "hole_spacing_y": 20.0,
zip_dan_jia.children[i].assign_child_resource(jipian2, location=None) # "hole_diameter": 5,
for i in range(6): # "info": "这是第一块料板"
jipian3 = ElectrodeSheet(name=f"zi_dan_jia3_jipian_{i}", size_x=12, size_y=12, size_z=0.1) # }
zip_dan_jia3.children[i].assign_child_resource(jipian3, location=None) # plate_1.update_locations()
for i in range(6): # ban_cao_wei.assign_child_resource(plate_1, Coordinate.zero())
jipian4 = ElectrodeSheet(name=f"zi_dan_jia4_jipian_{i}", size_x=12, size_y=12, size_z=0.1) # # zi_dan_jia = ClipMagazine("zi_dan_jia", 1, 1, 1)
zip_dan_jia4.children[i].assign_child_resource(jipian4, location=None) # # deck.assign_child_resource(ban_cao_wei, Coordinate(x=200, y=200, z=0))
for i in range(6): #
jipian5 = ElectrodeSheet(name=f"zi_dan_jia5_jipian_{i}", size_x=12, size_y=12, size_z=0.1) # from unilabos.resources.graphio import *
zip_dan_jia5.children[i].assign_child_resource(jipian5, location=None) # A = tree_to_list([resource_plr_to_ulab(deck)])
for i in range(6): # with open("test.json", "w") as f:
jipian6 = ElectrodeSheet(name=f"zi_dan_jia6_jipian_{i}", size_x=12, size_y=12, size_z=0.1) # json.dump(A, f)
zip_dan_jia6.children[i].assign_child_resource(jipian6, location=None) #
for i in range(6): #
jipian7 = ElectrodeSheet(name=f"zi_dan_jia7_jipian_{i}", size_x=12, size_y=12, size_z=0.1) #def get_plate_with_14mm_hole(name=""):
zip_dan_jia7.children[i].assign_child_resource(jipian7, location=None) # plate = MaterialPlate(name=name)
for i in range(6): # for i in range(4):
jipian8 = ElectrodeSheet(name=f"zi_dan_jia8_jipian_{i}", size_x=12, size_y=12, size_z=0.1) # for j in range(4):
zip_dan_jia8.children[i].assign_child_resource(jipian8, location=None) # hole = MaterialHole(f"{i+1}x{j+1}")
# hole._unilabos_state["diameter"] = 14
# ====================================== 物料板 ============================================ # hole._unilabos_state["max_sheets"] = 1
# 创建6个4*4的物料板 # plate.assign_child_resource(hole)
liaopan1 = MaterialPlate(name="liaopan1", size_x=120, size_y=100, size_z=10.0, fill=True) # return plate
self.assign_child_resource(liaopan1, Coordinate(x=1010, y=50, z=0))
for i in range(16):
jipian_1 = ElectrodeSheet(name=f"{liaopan1.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
liaopan1.children[i].assign_child_resource(jipian_1, location=None)
liaopan2 = MaterialPlate(name="liaopan2", size_x=120, size_y=100, size_z=10.0, fill=True) def create_a_liaopan():
self.assign_child_resource(liaopan2, Coordinate(x=1130, y=50, z=0)) liaopan = MaterialPlate(name="liaopan", size_x=120.8, size_y=120.5, size_z=10.0, fill=True)
for i in range(16):
jipian = ElectrodeSheet(name=f"jipian_{i}", size_x= 12, size_y=12, size_z=0.1)
liaopan1.children[i].assign_child_resource(jipian, location=None)
return liaopan
liaopan3 = MaterialPlate(name="liaopan3", size_x=120, size_y=100, size_z=10.0, fill=True) def create_a_coin_cell_deck():
self.assign_child_resource(liaopan3, Coordinate(x=1250, y=50, z=0)) deck = Deck(size_x=1200,
size_y=800,
size_z=900)
liaopan4 = MaterialPlate(name="liaopan4", size_x=120, size_y=100, size_z=10.0, fill=True) #liaopan = TipBox64(name="liaopan")
self.assign_child_resource(liaopan4, Coordinate(x=1010, y=150, z=0))
for i in range(16):
jipian_4 = ElectrodeSheet(name=f"{liaopan4.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
liaopan4.children[i].assign_child_resource(jipian_4, location=None)
liaopan5 = MaterialPlate(name="liaopan5", size_x=120, size_y=100, size_z=10.0, fill=True)
self.assign_child_resource(liaopan5, Coordinate(x=1130, y=150, z=0))
liaopan6 = MaterialPlate(name="liaopan6", size_x=120, size_y=100, size_z=10.0, fill=True)
self.assign_child_resource(liaopan6, Coordinate(x=1250, y=150, z=0))
# ====================================== 瓶架、移液枪 ============================================
# 在台面上放置 3x4 瓶架、6x2 瓶架 与 64孔移液枪头盒
bottle_rack_3x4 = BottleRack(
name="bottle_rack_3x4",
size_x=210.0,
size_y=140.0,
size_z=100.0,
num_items_x=3,
num_items_y=4,
position_spacing=35.0,
orientation="vertical",
)
self.assign_child_resource(bottle_rack_3x4, Coordinate(x=100, y=200, z=0))
bottle_rack_6x2 = BottleRack( #创建一个4*4的物料板
name="bottle_rack_6x2", liaopan1 = MaterialPlate(name="liaopan1", size_x=120.8, size_y=120.5, size_z=10.0, fill=True)
size_x=120.0, #把物料板放到桌子上
size_y=250.0, deck.assign_child_resource(liaopan1, Coordinate(x=0, y=0, z=0))
size_z=100.0, #创建一个极片
num_items_x=6, for i in range(16):
num_items_y=2, jipian = ElectrodeSheet(name=f"jipian_{i}", size_x= 12, size_y=12, size_z=0.1)
position_spacing=35.0, liaopan1.children[i].assign_child_resource(jipian, location=None)
orientation="vertical", #创建一个4*4的物料板
) liaopan2 = MaterialPlate(name="liaopan2", size_x=120.8, size_y=120.5, size_z=10.0, fill=True)
self.assign_child_resource(bottle_rack_6x2, Coordinate(x=300, y=300, z=0)) #把物料板放到桌子上
deck.assign_child_resource(liaopan2, Coordinate(x=500, y=0, z=0))
bottle_rack_6x2_2 = BottleRack( #创建一个4*4的物料板
name="bottle_rack_6x2_2", liaopan3 = MaterialPlate(name="liaopan3", size_x=120.8, size_y=120.5, size_z=10.0, fill=True)
size_x=120.0, #把物料板放到桌子上
size_y=250.0, deck.assign_child_resource(liaopan3, Coordinate(x=1000, y=0, z=0))
size_z=100.0,
num_items_x=6,
num_items_y=2,
position_spacing=35.0,
orientation="vertical",
)
self.assign_child_resource(bottle_rack_6x2_2, Coordinate(x=430, y=300, z=0))
# 将 ElectrodeSheet 放满 3x4 与 6x2 的所有孔位 print(deck)
for idx in range(bottle_rack_3x4.num_items_x * bottle_rack_3x4.num_items_y):
sheet = ElectrodeSheet(name=f"sheet_3x4_{idx}", size_x=12, size_y=12, size_z=0.1)
bottle_rack_3x4.assign_child_resource(sheet, index=idx)
for idx in range(bottle_rack_6x2.num_items_x * bottle_rack_6x2.num_items_y):
sheet = ElectrodeSheet(name=f"sheet_6x2_{idx}", size_x=12, size_y=12, size_z=0.1)
bottle_rack_6x2.assign_child_resource(sheet, index=idx)
tip_box = TipBox64(name="tip_box_64")
self.assign_child_resource(tip_box, Coordinate(x=300, y=100, z=0))
waste_tip_box = WasteTipBox(name="waste_tip_box")
self.assign_child_resource(waste_tip_box, Coordinate(x=300, y=200, z=0))
print(self)
def create_coin_cell_deck(name: str = "coin_cell_deck", size_x: float = 1000.0, size_y: float = 1000.0, size_z: float = 900.0) -> CoincellDeck:
"""创建并配置标准的纽扣电池组装工作站台面
Args:
name: 台面名称
size_x: 长度 (mm)
size_y: 宽度 (mm)
size_z: 高度 (mm)
Returns:
已配置好的 CoincellDeck 对象
"""
# 创建 CoincellDeck 实例并自动执行 setup 配置
deck = CoincellDeck(name=name, size_x=size_x, size_y=size_y, size_z=size_z, setup=True)
return deck return deck
import json
if __name__ == "__main__": if __name__ == "__main__":
deck = create_coin_cell_deck() electrode1 = BatteryPressSlot()
print(deck) #print(electrode1.get_size_x())
#print(electrode1.get_size_y())
#print(electrode1.get_size_z())
#jipian = ElectrodeSheet()
#jipian._unilabos_state["diameter"] = 18
#print(jipian.serialize())
#print(jipian.serialize_state())
deck = CoincellDeck()
"""======================================子弹夹============================================"""
zip_dan_jia = ClipMagazine_four("zi_dan_jia", 80, 80, 10)
deck.assign_child_resource(zip_dan_jia, Coordinate(x=1400, y=50, z=0))
zip_dan_jia2 = ClipMagazine_four("zi_dan_jia2", 80, 80, 10)
deck.assign_child_resource(zip_dan_jia2, Coordinate(x=1600, y=200, z=0))
zip_dan_jia3 = ClipMagazine("zi_dan_jia3", 80, 80, 10)
deck.assign_child_resource(zip_dan_jia3, Coordinate(x=1500, y=200, z=0))
zip_dan_jia4 = ClipMagazine("zi_dan_jia4", 80, 80, 10)
deck.assign_child_resource(zip_dan_jia4, Coordinate(x=1500, y=300, z=0))
zip_dan_jia5 = ClipMagazine("zi_dan_jia5", 80, 80, 10)
deck.assign_child_resource(zip_dan_jia5, Coordinate(x=1600, y=300, z=0))
zip_dan_jia6 = ClipMagazine("zi_dan_jia6", 80, 80, 10)
deck.assign_child_resource(zip_dan_jia6, Coordinate(x=1530, y=500, z=0))
zip_dan_jia7 = ClipMagazine("zi_dan_jia7", 80, 80, 10)
deck.assign_child_resource(zip_dan_jia7, Coordinate(x=1180, y=400, z=0))
zip_dan_jia8 = ClipMagazine("zi_dan_jia8", 80, 80, 10)
deck.assign_child_resource(zip_dan_jia8, Coordinate(x=1280, y=400, z=0))
for i in range(4):
jipian = ElectrodeSheet(name=f"zi_dan_jia_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia2.children[i].assign_child_resource(jipian, location=None)
for i in range(4):
jipian2 = ElectrodeSheet(name=f"zi_dan_jia2_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia.children[i].assign_child_resource(jipian2, location=None)
for i in range(6):
jipian3 = ElectrodeSheet(name=f"zi_dan_jia3_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia3.children[i].assign_child_resource(jipian3, location=None)
for i in range(6):
jipian4 = ElectrodeSheet(name=f"zi_dan_jia4_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia4.children[i].assign_child_resource(jipian4, location=None)
for i in range(6):
jipian5 = ElectrodeSheet(name=f"zi_dan_jia5_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia5.children[i].assign_child_resource(jipian5, location=None)
for i in range(6):
jipian6 = ElectrodeSheet(name=f"zi_dan_jia6_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia6.children[i].assign_child_resource(jipian6, location=None)
for i in range(6):
jipian7 = ElectrodeSheet(name=f"zi_dan_jia7_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia7.children[i].assign_child_resource(jipian7, location=None)
for i in range(6):
jipian8 = ElectrodeSheet(name=f"zi_dan_jia8_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
zip_dan_jia8.children[i].assign_child_resource(jipian8, location=None)
"""======================================子弹夹============================================"""
#liaopan = TipBox64(name="liaopan")
"""======================================物料板============================================"""
#创建一个4*4的物料板
liaopan1 = MaterialPlate(name="liaopan1", size_x=120, size_y=100, size_z=10.0, fill=True)
deck.assign_child_resource(liaopan1, Coordinate(x=1010, y=50, z=0))
for i in range(16):
jipian_1 = ElectrodeSheet(name=f"{liaopan1.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
liaopan1.children[i].assign_child_resource(jipian_1, location=None)
liaopan2 = MaterialPlate(name="liaopan2", size_x=120, size_y=100, size_z=10.0, fill=True)
deck.assign_child_resource(liaopan2, Coordinate(x=1130, y=50, z=0))
liaopan3 = MaterialPlate(name="liaopan3", size_x=120, size_y=100, size_z=10.0, fill=True)
deck.assign_child_resource(liaopan3, Coordinate(x=1250, y=50, z=0))
liaopan4 = MaterialPlate(name="liaopan4", size_x=120, size_y=100, size_z=10.0, fill=True)
deck.assign_child_resource(liaopan4, Coordinate(x=1010, y=150, z=0))
for i in range(16):
jipian_4 = ElectrodeSheet(name=f"{liaopan4.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1)
liaopan4.children[i].assign_child_resource(jipian_4, location=None)
liaopan5 = MaterialPlate(name="liaopan5", size_x=120, size_y=100, size_z=10.0, fill=True)
deck.assign_child_resource(liaopan5, Coordinate(x=1130, y=150, z=0))
liaopan6 = MaterialPlate(name="liaopan6", size_x=120, size_y=100, size_z=10.0, fill=True)
deck.assign_child_resource(liaopan6, Coordinate(x=1250, y=150, z=0))
#liaopan.children[3].assign_child_resource(jipian, location=None)
"""======================================物料板============================================"""
"""======================================瓶架,移液枪============================================"""
# 在台面上放置 3x4 瓶架、6x2 瓶架 与 64孔移液枪头盒
bottle_rack_3x4 = BottleRack(
name="bottle_rack_3x4",
size_x=210.0,
size_y=140.0,
size_z=100.0,
num_items_x=3,
num_items_y=4,
position_spacing=35.0,
orientation="vertical",
)
deck.assign_child_resource(bottle_rack_3x4, Coordinate(x=100, y=200, z=0))
bottle_rack_6x2 = BottleRack(
name="bottle_rack_6x2",
size_x=120.0,
size_y=250.0,
size_z=100.0,
num_items_x=6,
num_items_y=2,
position_spacing=35.0,
orientation="vertical",
)
deck.assign_child_resource(bottle_rack_6x2, Coordinate(x=300, y=300, z=0))
bottle_rack_6x2_2 = BottleRack(
name="bottle_rack_6x2_2",
size_x=120.0,
size_y=250.0,
size_z=100.0,
num_items_x=6,
num_items_y=2,
position_spacing=35.0,
orientation="vertical",
)
deck.assign_child_resource(bottle_rack_6x2_2, Coordinate(x=430, y=300, z=0))
# 将 ElectrodeSheet 放满 3x4 与 6x2 的所有孔位
for idx in range(bottle_rack_3x4.num_items_x * bottle_rack_3x4.num_items_y):
sheet = ElectrodeSheet(name=f"sheet_3x4_{idx}", size_x=12, size_y=12, size_z=0.1)
bottle_rack_3x4.assign_child_resource(sheet, index=idx)
for idx in range(bottle_rack_6x2.num_items_x * bottle_rack_6x2.num_items_y):
sheet = ElectrodeSheet(name=f"sheet_6x2_{idx}", size_x=12, size_y=12, size_z=0.1)
bottle_rack_6x2.assign_child_resource(sheet, index=idx)
tip_box = TipBox64(name="tip_box_64")
deck.assign_child_resource(tip_box, Coordinate(x=300, y=100, z=0))
waste_tip_box = WasteTipBox(name="waste_tip_box")
deck.assign_child_resource(waste_tip_box, Coordinate(x=300, y=200, z=0))
"""======================================瓶架,移液枪============================================"""
print(deck)
from unilabos.resources.graphio import convert_resources_from_type
from unilabos.config.config import BasicConfig
BasicConfig.ak = "56bbed5b-6e30-438c-b06d-f69eaa63bb45"
BasicConfig.sk = "238222fe-0bf7-4350-a426-e5ced8011dcf"
from unilabos.app.web.client import http_client
resources = convert_resources_from_type([deck], [Resource])
# 检查序列化后的资源
json.dump({"nodes": resources, "links": []}, open("button_battery_decks_unilab.json", "w"), indent=2)
#print(resources)
http_client.remote_addr = "https://uni-lab.test.bohrium.com/api/v1"
http_client.resource_add(resources)

View File

@@ -1,133 +1,33 @@
import csv import csv
import inspect
import json import json
import os import os
import threading import threading
import time import time
import types
from datetime import datetime from datetime import datetime
from typing import Any, Dict, Optional from typing import Any, Dict, Optional
from functools import wraps from pylabrobot.resources import Resource as PLRResource
from pylabrobot.resources import Deck, Resource as PLRResource
from unilabos_msgs.msg import Resource from unilabos_msgs.msg import Resource
from unilabos.device_comms.modbus_plc.client import ModbusTcpClient from unilabos.device_comms.modbus_plc.client import ModbusTcpClient
from unilabos.devices.workstation.coin_cell_assembly.button_battery_station import MaterialHole, MaterialPlate
from unilabos.devices.workstation.workstation_base import WorkstationBase from unilabos.devices.workstation.workstation_base import WorkstationBase
from unilabos.device_comms.modbus_plc.client import TCPClient, ModbusNode, PLCWorkflow, ModbusWorkflow, WorkflowAction, BaseClient from unilabos.device_comms.modbus_plc.client import TCPClient, ModbusNode, PLCWorkflow, ModbusWorkflow, WorkflowAction, BaseClient
from unilabos.device_comms.modbus_plc.modbus import DeviceType, Base as ModbusNodeBase, DataType, WorderOrder from unilabos.device_comms.modbus_plc.modbus import DeviceType, Base as ModbusNodeBase, DataType, WorderOrder
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import * from unilabos.devices.workstation.coin_cell_assembly.button_battery_station import *
from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode
from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode
from unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials import CoincellDeck
from unilabos.resources.graphio import convert_resources_to_type
from unilabos.utils.log import logger
def _ensure_modbus_slave_kw_alias(modbus_client):
if modbus_client is None:
return
method_names = [
"read_coils",
"write_coils",
"write_coil",
"read_discrete_inputs",
"read_holding_registers",
"write_register",
"write_registers",
]
def _wrap(func):
signature = inspect.signature(func)
has_var_kwargs = any(param.kind == param.VAR_KEYWORD for param in signature.parameters.values())
accepts_unit = has_var_kwargs or "unit" in signature.parameters
accepts_slave = has_var_kwargs or "slave" in signature.parameters
@wraps(func)
def _wrapped(self, *args, **kwargs):
if "slave" in kwargs and not accepts_slave:
slave_value = kwargs.pop("slave")
if accepts_unit and "unit" not in kwargs:
kwargs["unit"] = slave_value
if "unit" in kwargs and not accepts_unit:
unit_value = kwargs.pop("unit")
if accepts_slave and "slave" not in kwargs:
kwargs["slave"] = unit_value
return func(self, *args, **kwargs)
_wrapped._has_slave_alias = True
return _wrapped
for name in method_names:
if not hasattr(modbus_client, name):
continue
bound_method = getattr(modbus_client, name)
func = getattr(bound_method, "__func__", None)
if func is None:
continue
if getattr(func, "_has_slave_alias", False):
continue
wrapped = _wrap(func)
setattr(modbus_client, name, types.MethodType(wrapped, modbus_client))
def _coerce_deck_input(deck: Any) -> Optional[Deck]:
if deck is None:
return None
if isinstance(deck, Deck):
return deck
if isinstance(deck, PLRResource):
return deck if isinstance(deck, Deck) else None
candidates = None
if isinstance(deck, dict):
if "nodes" in deck and isinstance(deck["nodes"], list):
candidates = deck["nodes"]
else:
candidates = [deck]
elif isinstance(deck, list):
candidates = deck
if candidates is None:
return None
try:
converted = convert_resources_to_type(resources_list=candidates, resource_type=Deck)
if isinstance(converted, Deck):
return converted
if isinstance(converted, list):
for item in converted:
if isinstance(item, Deck):
return item
except Exception as exc:
logger.warning(f"deck 转换 Deck 失败: {exc}")
return None
#构建物料系统 #构建物料系统
class CoinCellAssemblyWorkstation(WorkstationBase): class CoinCellAssemblyWorkstation(WorkstationBase):
def __init__( def __init__(
self, self,
deck: Deck=None, deck: CoincellDeck,
address: str = "172.16.28.102", address: str = "192.168.1.20",
port: str = "502", port: str = "502",
debug_mode: bool = False, debug_mode: bool = True,
*args, *args,
**kwargs, **kwargs,
): ):
if deck is None and "deck" in kwargs:
deck = kwargs.pop("deck")
else:
kwargs.pop("deck", None)
normalized_deck = _coerce_deck_input(deck)
if deck is None and isinstance(normalized_deck, Deck):
deck = normalized_deck
super().__init__( super().__init__(
#桌子 #桌子
deck=deck, deck=deck,
@@ -135,22 +35,10 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
**kwargs, **kwargs,
) )
self.debug_mode = debug_mode self.debug_mode = debug_mode
self.deck = deck
# 如果没有传入 deck则创建标准配置的 deck
if self.deck is None:
self.deck = CoincellDeck(size_x=1000, size_y=1000, size_z=900, origin=Coordinate(-800, 0, 0),setup=True)
else:
# 如果传入了 deck 但还没有 setup可以选择是否 setup
if self.deck is not None and len(self.deck.children) == 0:
# deck 为空,执行 setup
self.deck.setup()
# 否则使用传入的 deck可能已经配置好了
self.deck = self.deck
""" 连接初始化 """ """ 连接初始化 """
modbus_client = TCPClient(addr=address, port=port) modbus_client = TCPClient(addr=address, port=port)
logger.debug(f"创建 Modbus 客户端: {modbus_client}") print("modbus_client", modbus_client)
_ensure_modbus_slave_kw_alias(modbus_client.client)
if not debug_mode: if not debug_mode:
modbus_client.client.connect() modbus_client.client.connect()
count = 100 count = 100
@@ -165,15 +53,16 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
print("测试模式,跳过连接") print("测试模式,跳过连接")
""" 工站的配置 """ """ 工站的配置 """
self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_1105.csv')) self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_a.csv'))
self.client = modbus_client.register_node_list(self.nodes) self.client = modbus_client.register_node_list(self.nodes)
self.success = False self.success = False
self.allow_data_read = False #允许读取函数运行标志位 self.allow_data_read = False #允许读取函数运行标志位
self.csv_export_thread = None self.csv_export_thread = None
self.csv_export_running = False self.csv_export_running = False
self.csv_export_file = None self.csv_export_file = None
self.coin_num_N = 0 #已组装电池数量
#创建一个物料台面,包含两个极片板 #创建一个物料台面,包含两个极片板
#self.deck = create_a_coin_cell_deck()
#self._ros_node.update_resource(self.deck) #self._ros_node.update_resource(self.deck)
#ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ #ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
@@ -602,11 +491,11 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
try: try:
# 尝试不同的字节序读取 # 尝试不同的字节序读取
code_little, read_err = self.client.use_node('REG_DATA_COIN_CELL_CODE').read(10, word_order=WorderOrder.LITTLE) code_little, read_err = self.client.use_node('REG_DATA_COIN_CELL_CODE').read(10, word_order=WorderOrder.LITTLE)
# logger.debug(f"读取电池二维码原始数据: {code_little}") print(code_little)
clean_code = code_little[-8:][::-1] clean_code = code_little[-8:][::-1]
return clean_code return clean_code
except Exception as e: except Exception as e:
logger.error(f"读取电池二维码失败: {e}") print(f"读取电池二维码失败: {e}")
return "N/A" return "N/A"
@@ -615,11 +504,11 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
try: try:
# 尝试不同的字节序读取 # 尝试不同的字节序读取
code_little, read_err = self.client.use_node('REG_DATA_ELECTROLYTE_CODE').read(10, word_order=WorderOrder.LITTLE) code_little, read_err = self.client.use_node('REG_DATA_ELECTROLYTE_CODE').read(10, word_order=WorderOrder.LITTLE)
# logger.debug(f"读取电解液二维码原始数据: {code_little}") print(code_little)
clean_code = code_little[-8:][::-1] clean_code = code_little[-8:][::-1]
return clean_code return clean_code
except Exception as e: except Exception as e:
logger.error(f"读取电解液二维码失败: {e}") print(f"读取电解液二维码失败: {e}")
return "N/A" return "N/A"
# ===================== 环境监控区 ====================== # ===================== 环境监控区 ======================
@@ -717,8 +606,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
print("waiting for start_cmd") print("waiting for start_cmd")
time.sleep(1) time.sleep(1)
def func_pack_send_bottle_num(self, bottle_num): def func_pack_send_bottle_num(self, bottle_num: int):
bottle_num = int(bottle_num)
#发送电解液平台数 #发送电解液平台数
print("启动") print("启动")
while (self._unilab_rece_electrolyte_bottle_num()) == False: while (self._unilab_rece_electrolyte_bottle_num()) == False:
@@ -766,25 +654,16 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
# self.success = True # self.success = True
# return self.success # return self.success
def func_pack_send_msg_cmd(self, elec_use_num, elec_vol, assembly_type, assembly_pressure) -> bool: def func_pack_send_msg_cmd(self, elec_use_num) -> bool:
"""UNILAB写参数""" """UNILAB写参数"""
while (self.request_rec_msg_status) == False: while (self.request_rec_msg_status) == False:
print("wait for request_rec_msg_status to True") print("wait for request_rec_msg_status to True")
time.sleep(1) time.sleep(1)
self.success = False self.success = False
#self._unilab_send_msg_electrolyte_num(elec_num) #self._unilab_send_msg_electrolyte_num(elec_num)
#设置平行样数目 time.sleep(1)
self._unilab_send_msg_electrolyte_use_num(elec_use_num) self._unilab_send_msg_electrolyte_use_num(elec_use_num)
time.sleep(1) time.sleep(1)
#发送电解液加注量
self._unilab_send_msg_electrolyte_vol(elec_vol)
time.sleep(1)
#发送电解液组装类型
self._unilab_send_msg_assembly_type(assembly_type)
time.sleep(1)
#发送电池压制力
self._unilab_send_msg_assembly_pressure(assembly_pressure)
time.sleep(1)
self._unilab_send_msg_succ_cmd(True) self._unilab_send_msg_succ_cmd(True)
time.sleep(1) time.sleep(1)
while (self.request_rec_msg_status) == True: while (self.request_rec_msg_status) == True:
@@ -809,32 +688,15 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
data_coin_num = self.data_coin_num data_coin_num = self.data_coin_num
data_electrolyte_code = self.data_electrolyte_code data_electrolyte_code = self.data_electrolyte_code
data_coin_cell_code = self.data_coin_cell_code data_coin_cell_code = self.data_coin_cell_code
logger.debug(f"data_open_circuit_voltage: {data_open_circuit_voltage}") print("data_open_circuit_voltage", data_open_circuit_voltage)
logger.debug(f"data_pole_weight: {data_pole_weight}") print("data_pole_weight", data_pole_weight)
logger.debug(f"data_assembly_time: {data_assembly_time}") print("data_assembly_time", data_assembly_time)
logger.debug(f"data_assembly_pressure: {data_assembly_pressure}") print("data_assembly_pressure", data_assembly_pressure)
logger.debug(f"data_electrolyte_volume: {data_electrolyte_volume}") print("data_electrolyte_volume", data_electrolyte_volume)
logger.debug(f"data_coin_num: {data_coin_num}") print("data_coin_num", data_coin_num)
logger.debug(f"data_electrolyte_code: {data_electrolyte_code}") print("data_electrolyte_code", data_electrolyte_code)
logger.debug(f"data_coin_cell_code: {data_coin_cell_code}") print("data_coin_cell_code", data_coin_cell_code)
#接收完信息后读取完毕标志位置True #接收完信息后读取完毕标志位置True
liaopan3 = self.deck.get_resource("\u7535\u6c60\u6599\u76d8")
#把物料解绑后放到另一盘上
battery = ElectrodeSheet(name=f"battery_{self.coin_num_N}", size_x=14, size_y=14, size_z=2)
battery._unilabos_state = {
"electrolyte_name": data_coin_cell_code,
"data_electrolyte_code": data_electrolyte_code,
"open_circuit_voltage": data_open_circuit_voltage,
"assembly_pressure": data_assembly_pressure,
"electrolyte_volume": data_electrolyte_volume
}
liaopan3.children[self.coin_num_N].assign_child_resource(battery, location=None)
#print(jipian2.parent)
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
"resources": [self.deck]
})
self._unilab_rec_msg_succ_cmd(True) self._unilab_rec_msg_succ_cmd(True)
time.sleep(1) time.sleep(1)
#等待允许读取标志位置False #等待允许读取标志位置False
@@ -892,25 +754,11 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
self.success = True self.success = True
return self.success return self.success
def qiming_coin_cell_code(self, fujipian_panshu:int, fujipian_juzhendianwei:int=0, gemopanshu:int=0, gemo_juzhendianwei:int=0, lvbodian:bool=True, battery_pressure_mode:bool=True, battery_pressure:int=4000, battery_clean_ignore:bool=False) -> bool:
self.success = False
self.client.use_node('REG_MSG_NE_PLATE_NUM').write(fujipian_panshu)
self.client.use_node('REG_MSG_NE_PLATE_MATRIX').write(fujipian_juzhendianwei)
self.client.use_node('REG_MSG_SEPARATOR_PLATE_NUM').write(gemopanshu)
self.client.use_node('REG_MSG_SEPARATOR_PLATE_MATRIX').write(gemo_juzhendianwei)
self.client.use_node('COIL_ALUMINUM_FOIL').write(not lvbodian)
self.client.use_node('REG_MSG_PRESS_MODE').write(not battery_pressure_mode)
# self.client.use_node('REG_MSG_ASSEMBLY_PRESSURE').write(battery_pressure)
self.client.use_node('REG_MSG_BATTERY_CLEAN_IGNORE').write(battery_clean_ignore)
self.success = True
return self.success
def func_allpack_cmd(self, elec_num, elec_use_num, elec_vol:int=50, assembly_type:int=7, assembly_pressure:int=4200, file_path: str="/Users/sml/work") -> bool:
elec_num, elec_use_num, elec_vol, assembly_type, assembly_pressure = int(elec_num), int(elec_use_num), int(elec_vol), int(assembly_type), int(assembly_pressure) def func_allpack_cmd(self, elec_num, elec_use_num, file_path: str="D:\\coin_cell_data") -> bool:
summary_csv_file = os.path.join(file_path, "duandian.csv") summary_csv_file = os.path.join(file_path, "duandian.csv")
# 如果断点文件存在,先读取之前的进度 # 如果断点文件存在,先读取之前的进度
if os.path.exists(summary_csv_file): if os.path.exists(summary_csv_file):
read_status_flag = True read_status_flag = True
with open(summary_csv_file, 'r', newline='', encoding='utf-8') as csvfile: with open(summary_csv_file, 'r', newline='', encoding='utf-8') as csvfile:
@@ -936,38 +784,54 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
elec_num_N = 0 elec_num_N = 0
elec_use_num_N = 0 elec_use_num_N = 0
coin_num_N = 0 coin_num_N = 0
for i in range(20):
print(f"剩余电解液瓶数: {elec_num}, 已组装电池数: {elec_use_num}") print(f"剩余电解液瓶数: {elec_num}, 已组装电池数: {elec_use_num}")
print(f"剩余电解液瓶数: {type(elec_num)}, 已组装电池数: {type(elec_use_num)}")
print(f"剩余电解液瓶数: {type(int(elec_num))}, 已组装电池数: {type(int(elec_use_num))}")
#如果是第一次运行,则进行初始化、切换自动、启动, 如果是断点重启则跳过。 #如果是第一次运行,则进行初始化、切换自动、启动, 如果是断点重启则跳过。
if read_status_flag == False: if read_status_flag == False:
pass
#初始化 #初始化
#self.func_pack_device_init() self.func_pack_device_init()
#切换自动 #切换自动
#self.func_pack_device_auto() self.func_pack_device_auto()
#启动,小车收回 #启动,小车收回
#self.func_pack_device_start() self.func_pack_device_start()
#发送电解液瓶数量,启动搬运,多搬运没事 #发送电解液瓶数量,启动搬运,多搬运没事
#self.func_pack_send_bottle_num(elec_num) self.func_pack_send_bottle_num(elec_num)
last_i = elec_num_N last_i = elec_num_N
last_j = elec_use_num_N last_j = elec_use_num_N
for i in range(last_i, elec_num): for i in range(last_i, elec_num):
print(f"开始第{last_i+i+1}瓶电解液的组装") print(f"开始第{last_i+i+1}瓶电解液的组装")
#第一个循环从上次断点继续后续循环从0开始 #第一个循环从上次断点继续后续循环从0开始
j_start = last_j if i == last_i else 0 j_start = last_j if i == last_i else 0
self.func_pack_send_msg_cmd(elec_use_num-j_start, elec_vol, assembly_type, assembly_pressure) self.func_pack_send_msg_cmd(elec_use_num-j_start)
for j in range(j_start, elec_use_num): for j in range(j_start, elec_use_num):
print(f"开始第{last_i+i+1}瓶电解液的第{j+j_start+1}个电池组装") print(f"开始第{last_i+i+1}瓶电解液的第{j+j_start+1}个电池组装")
#读取电池组装数据并存入csv #读取电池组装数据并存入csv
self.func_pack_get_msg_cmd(file_path) self.func_pack_get_msg_cmd(file_path)
time.sleep(1) time.sleep(1)
#这里定义物料系统
# TODO:读完再将电池数加一还是进入循环就将电池数加一需要考虑 # TODO:读完再将电池数加一还是进入循环就将电池数加一需要考虑
liaopan1 = self.deck.get_resource("liaopan1")
liaopan4 = self.deck.get_resource("liaopan4")
jipian1 = liaopan1.children[coin_num_N].children[0]
jipian4 = liaopan4.children[coin_num_N].children[0]
#print(jipian1)
#从料盘上去物料解绑后放到另一盘上
jipian1.parent.unassign_child_resource(jipian1)
jipian4.parent.unassign_child_resource(jipian4)
#print(jipian2.parent)
battery = Battery(name = f"battery_{coin_num_N}")
battery.assign_child_resource(jipian1, location=None)
battery.assign_child_resource(jipian4, location=None)
zidanjia6 = self.deck.get_resource("zi_dan_jia6")
zidanjia6.children[0].assign_child_resource(battery, location=None)
# 生成断点文件 # 生成断点文件
# 生成包含elec_num_N、coin_num_N、timestamp的CSV文件 # 生成包含elec_num_N、coin_num_N、timestamp的CSV文件
@@ -978,7 +842,6 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
writer.writerow([elec_num, elec_use_num, elec_num_N, elec_use_num_N, coin_num_N, timestamp]) writer.writerow([elec_num, elec_use_num, elec_num_N, elec_use_num_N, coin_num_N, timestamp])
csvfile.flush() csvfile.flush()
coin_num_N += 1 coin_num_N += 1
self.coin_num_N = coin_num_N
elec_use_num_N += 1 elec_use_num_N += 1
elec_num_N += 1 elec_num_N += 1
elec_use_num_N = 0 elec_use_num_N = 0
@@ -1015,25 +878,34 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
def fun_wuliao_test(self) -> bool: def fun_wuliao_test(self) -> bool:
#找到data_init中构建的2个物料盘 #找到data_init中构建的2个物料盘
liaopan3 = self.deck.get_resource("\u7535\u6c60\u6599\u76d8") #liaopan1 = self.deck.get_resource("liaopan1")
for i in range(16): #liaopan4 = self.deck.get_resource("liaopan4")
battery = ElectrodeSheet(name=f"battery_{i}", size_x=16, size_y=16, size_z=2) #for coin_num_N in range(16):
battery._unilabos_state = { # liaopan1 = self.deck.get_resource("liaopan1")
"diameter": 20.0, # liaopan4 = self.deck.get_resource("liaopan4")
"height": 20.0, # jipian1 = liaopan1.children[coin_num_N].children[0]
"assembly_pressure": i, # jipian4 = liaopan4.children[coin_num_N].children[0]
"electrolyte_volume": 20.0, # #print(jipian1)
"electrolyte_name": f"DP{i}" # #从料盘上去物料解绑后放到另一盘上
} # jipian1.parent.unassign_child_resource(jipian1)
liaopan3.children[i].assign_child_resource(battery, location=None) # jipian4.parent.unassign_child_resource(jipian4)
#
# #print(jipian2.parent)
# battery = Battery(name = f"battery_{coin_num_N}")
# battery.assign_child_resource(jipian1, location=None)
# battery.assign_child_resource(jipian4, location=None)
#
# zidanjia6 = self.deck.get_resource("zi_dan_jia6")
# zidanjia6.children[0].assign_child_resource(battery, location=None)
# ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
# "resources": [self.deck]
# })
# time.sleep(2)
for i in range(20):
print(f"输出{i}")
time.sleep(2)
ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{
"resources": [self.deck]
})
# for i in range(40):
# print(f"fun_wuliao_test 运行结束{i}")
# time.sleep(1)
# time.sleep(40)
# 数据读取与输出 # 数据读取与输出
def func_read_data_and_output(self, file_path: str="D:\\coin_cell_data"): def func_read_data_and_output(self, file_path: str="D:\\coin_cell_data"):
# 检查CSV导出是否正在运行已运行则跳出防止同时启动两个while循环 # 检查CSV导出是否正在运行已运行则跳出防止同时启动两个while循环
@@ -1140,7 +1012,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
# else: # else:
# print("子弹夹洞位0没有极片") # print("子弹夹洞位0没有极片")
# #
# # TODO:#把电解液从瓶中取到电池夹子中 # #把电解液从瓶中取到电池夹子中
# battery_site = deck.get_resource("battery_press_1") # battery_site = deck.get_resource("battery_press_1")
# clip_magazine_battery = deck.get_resource("clip_magazine_battery") # clip_magazine_battery = deck.get_resource("clip_magazine_battery")
# if battery_site.has_battery(): # if battery_site.has_battery():
@@ -1230,10 +1102,41 @@ class CoinCellAssemblyWorkstation(WorkstationBase):
''' '''
if __name__ == "__main__": if __name__ == "__main__":
# 简单测试 from pylabrobot.resources import Resource
workstation = CoinCellAssemblyWorkstation() Coin_Cell = CoinCellAssemblyWorkstation(Resource("1", 1, 1, 1), debug_mode=True)
workstation.qiming_coin_cell_code(fujipian_panshu=1, fujipian_juzhendianwei=2, gemopanshu=3, gemo_juzhendianwei=4, lvbodian=False, battery_pressure_mode=False, battery_pressure=4200, battery_clean_ignore=False) #Coin_Cell.func_pack_device_init()
print(f"工作站创建成功: {workstation.deck.name}") #Coin_Cell.func_pack_device_auto()
print(f"料盘数量: {len(workstation.deck.children)}") #Coin_Cell.func_pack_device_start()
#Coin_Cell.func_pack_send_bottle_num(2)
#Coin_Cell.func_pack_send_msg_cmd(2)
#Coin_Cell.func_pack_get_msg_cmd()
#Coin_Cell.func_pack_get_msg_cmd()
#Coin_Cell.func_pack_send_finished_cmd()
#
#Coin_Cell.func_allpack_cmd(3, 2)
#print(Coin_Cell.data_stack_vision_code)
#print("success")
#创建一个物料台面
#deck = create_a_coin_cell_deck()
##在台面上找到料盘和极片
#liaopan1 = deck.get_resource("liaopan1")
#liaopan2 = deck.get_resource("liaopan2")
#jipian1 = liaopan1.children[1].children[0]
#
##print(jipian1)
##把物料解绑后放到另一盘上
#jipian1.parent.unassign_child_resource(jipian1)
#liaopan2.children[1].assign_child_resource(jipian1, location=None)
##print(jipian2.parent)
from unilabos.resources.graphio import resource_ulab_to_plr, convert_resources_to_type
with open("./button_battery_decks_unilab.json", "r", encoding="utf-8") as f:
bioyond_resources_unilab = json.load(f)
print(f"成功读取 JSON 文件,包含 {len(bioyond_resources_unilab)} 个资源")
ulab_resources = convert_resources_to_type(bioyond_resources_unilab, List[PLRResource])
print(f"转换结果类型: {type(ulab_resources)}")
print(ulab_resources)

View File

@@ -1,64 +0,0 @@
Name,DataType,InitValue,Comment,Attribute,DeviceType,Address,
COIL_SYS_START_CMD,BOOL,,,,coil,9010,
COIL_SYS_STOP_CMD,BOOL,,,,coil,9020,
COIL_SYS_RESET_CMD,BOOL,,,,coil,9030,
COIL_SYS_HAND_CMD,BOOL,,,,coil,9040,
COIL_SYS_AUTO_CMD,BOOL,,,,coil,9050,
COIL_SYS_INIT_CMD,BOOL,,,,coil,9060,
COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,,,,coil,9700,
COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,,,,coil,9710,unilab_rec_msg_succ_cmd
COIL_SYS_START_STATUS,BOOL,,,,coil,9210,
COIL_SYS_STOP_STATUS,BOOL,,,,coil,9220,
COIL_SYS_RESET_STATUS,BOOL,,,,coil,9230,
COIL_SYS_HAND_STATUS,BOOL,,,,coil,9240,
COIL_SYS_AUTO_STATUS,BOOL,,,,coil,9250,
COIL_SYS_INIT_STATUS,BOOL,,,,coil,9260,
COIL_REQUEST_REC_MSG_STATUS,BOOL,,,,coil,9500,
COIL_REQUEST_SEND_MSG_STATUS,BOOL,,,,coil,9510,request_send_msg_status
REG_MSG_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,17000,
REG_MSG_ELECTROLYTE_NUM,INT16,,,,hold_register,17002,unilab_send_msg_electrolyte_num
REG_MSG_ELECTROLYTE_VOLUME,INT16,,,,hold_register,17004,unilab_send_msg_electrolyte_vol
REG_MSG_ASSEMBLY_TYPE,INT16,,,,hold_register,17006,unilab_send_msg_assembly_type
REG_MSG_ASSEMBLY_PRESSURE,INT16,,,,hold_register,17008,unilab_send_msg_assembly_pressure
REG_DATA_ASSEMBLY_COIN_CELL_NUM,INT16,,,,hold_register,16000,data_assembly_coin_cell_num
REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,,,,hold_register,16002,data_open_circuit_voltage
REG_DATA_AXIS_X_POS,FLOAT32,,,,hold_register,16004,
REG_DATA_AXIS_Y_POS,FLOAT32,,,,hold_register,16006,
REG_DATA_AXIS_Z_POS,FLOAT32,,,,hold_register,16008,
REG_DATA_POLE_WEIGHT,FLOAT32,,,,hold_register,16010,data_pole_weight
REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,,,,hold_register,16012,data_assembly_time
REG_DATA_ASSEMBLY_PRESSURE,INT16,,,,hold_register,16014,data_assembly_pressure
REG_DATA_ELECTROLYTE_VOLUME,INT16,,,,hold_register,16016,data_electrolyte_volume
REG_DATA_COIN_NUM,INT16,,,,hold_register,16018,data_coin_num
REG_DATA_ELECTROLYTE_CODE,STRING,,,,hold_register,16020,data_electrolyte_code()
REG_DATA_COIN_CELL_CODE,STRING,,,,hold_register,16030,data_coin_cell_code()
REG_DATA_STACK_VISON_CODE,STRING,,,,hold_register,18004,data_stack_vision_code()
REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,,,,hold_register,16050,data_glove_box_pressure
REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,,,,hold_register,16052,data_glove_box_water_content
REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,,,hold_register,16054,data_glove_box_o2_content
UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,9720,
UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,9520,
REG_MSG_ELECTROLYTE_NUM_USED,INT16,,,,hold_register,17496,
REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,16000,
UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,9730,
UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,9530,
REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,16018,ASSEMBLY_TYPE7or8
COIL_ALUMINUM_FOIL,BOOL,,使用铝箔垫,,coil,9340,
REG_MSG_NE_PLATE_MATRIX,INT16,,负极片矩阵点位,,hold_register,17440,
REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,隔膜矩阵点位,,hold_register,17450,
REG_MSG_TIP_BOX_MATRIX,INT16,,移液枪头矩阵点位,,hold_register,17480,
REG_MSG_NE_PLATE_NUM,INT16,,负极片盘数,,hold_register,17443,
REG_MSG_SEPARATOR_PLATE_NUM,INT16,,隔膜盘数,,hold_register,17453,
REG_MSG_PRESS_MODE,BOOL,,压制模式false:压力检测模式True:距离模式),,coil,9360,电池压制模式
,,,,,,,
,BOOL,,视觉对位false:使用true:忽略),,coil,9300,视觉对位
,BOOL,,复检false:使用true:忽略),,coil,9310,视觉复检
,BOOL,,手套箱_左仓false:使用true:忽略),,coil,9320,手套箱左仓
,BOOL,,手套箱_右仓false:使用true:忽略),,coil,9420,手套箱右仓
,BOOL,,真空检知false:使用true:忽略),,coil,9350,真空检知
,BOOL,,电解液添加模式false:单次滴液true:二次滴液),,coil,9370,滴液模式
,BOOL,,正极片称重false:使用true:忽略),,coil,9380,正极片称重
,BOOL,,正负极片组装方式false:正装true:倒装),,coil,9390,正负极反装
,BOOL,,压制清洁false:使用true:忽略),,coil,9400,压制清洁
,BOOL,,物料盘摆盘方式false:水平摆盘true:堆叠摆盘),,coil,9410,负极片摆盘方式
REG_MSG_BATTERY_CLEAN_IGNORE,BOOL,,忽略电池清洁false:使用true:忽略),,coil,9460,
1 Name DataType InitValue Comment Attribute DeviceType Address
2 COIL_SYS_START_CMD BOOL coil 9010
3 COIL_SYS_STOP_CMD BOOL coil 9020
4 COIL_SYS_RESET_CMD BOOL coil 9030
5 COIL_SYS_HAND_CMD BOOL coil 9040
6 COIL_SYS_AUTO_CMD BOOL coil 9050
7 COIL_SYS_INIT_CMD BOOL coil 9060
8 COIL_UNILAB_SEND_MSG_SUCC_CMD BOOL coil 9700
9 COIL_UNILAB_REC_MSG_SUCC_CMD BOOL coil 9710 unilab_rec_msg_succ_cmd
10 COIL_SYS_START_STATUS BOOL coil 9210
11 COIL_SYS_STOP_STATUS BOOL coil 9220
12 COIL_SYS_RESET_STATUS BOOL coil 9230
13 COIL_SYS_HAND_STATUS BOOL coil 9240
14 COIL_SYS_AUTO_STATUS BOOL coil 9250
15 COIL_SYS_INIT_STATUS BOOL coil 9260
16 COIL_REQUEST_REC_MSG_STATUS BOOL coil 9500
17 COIL_REQUEST_SEND_MSG_STATUS BOOL coil 9510 request_send_msg_status
18 REG_MSG_ELECTROLYTE_USE_NUM INT16 hold_register 17000
19 REG_MSG_ELECTROLYTE_NUM INT16 hold_register 17002 unilab_send_msg_electrolyte_num
20 REG_MSG_ELECTROLYTE_VOLUME INT16 hold_register 17004 unilab_send_msg_electrolyte_vol
21 REG_MSG_ASSEMBLY_TYPE INT16 hold_register 17006 unilab_send_msg_assembly_type
22 REG_MSG_ASSEMBLY_PRESSURE INT16 hold_register 17008 unilab_send_msg_assembly_pressure
23 REG_DATA_ASSEMBLY_COIN_CELL_NUM INT16 hold_register 16000 data_assembly_coin_cell_num
24 REG_DATA_OPEN_CIRCUIT_VOLTAGE FLOAT32 hold_register 16002 data_open_circuit_voltage
25 REG_DATA_AXIS_X_POS FLOAT32 hold_register 16004
26 REG_DATA_AXIS_Y_POS FLOAT32 hold_register 16006
27 REG_DATA_AXIS_Z_POS FLOAT32 hold_register 16008
28 REG_DATA_POLE_WEIGHT FLOAT32 hold_register 16010 data_pole_weight
29 REG_DATA_ASSEMBLY_PER_TIME FLOAT32 hold_register 16012 data_assembly_time
30 REG_DATA_ASSEMBLY_PRESSURE INT16 hold_register 16014 data_assembly_pressure
31 REG_DATA_ELECTROLYTE_VOLUME INT16 hold_register 16016 data_electrolyte_volume
32 REG_DATA_COIN_NUM INT16 hold_register 16018 data_coin_num
33 REG_DATA_ELECTROLYTE_CODE STRING hold_register 16020 data_electrolyte_code()
34 REG_DATA_COIN_CELL_CODE STRING hold_register 16030 data_coin_cell_code()
35 REG_DATA_STACK_VISON_CODE STRING hold_register 18004 data_stack_vision_code()
36 REG_DATA_GLOVE_BOX_PRESSURE FLOAT32 hold_register 16050 data_glove_box_pressure
37 REG_DATA_GLOVE_BOX_WATER_CONTENT FLOAT32 hold_register 16052 data_glove_box_water_content
38 REG_DATA_GLOVE_BOX_O2_CONTENT FLOAT32 hold_register 16054 data_glove_box_o2_content
39 UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM BOOL coil 9720
40 UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM BOOL coil 9520
41 REG_MSG_ELECTROLYTE_NUM_USED INT16 hold_register 17496
42 REG_DATA_ELECTROLYTE_USE_NUM INT16 hold_register 16000
43 UNILAB_SEND_FINISHED_CMD BOOL coil 9730
44 UNILAB_RECE_FINISHED_CMD BOOL coil 9530
45 REG_DATA_ASSEMBLY_TYPE INT16 hold_register 16018 ASSEMBLY_TYPE7or8
46 COIL_ALUMINUM_FOIL BOOL 使用铝箔垫 coil 9340
47 REG_MSG_NE_PLATE_MATRIX INT16 负极片矩阵点位 hold_register 17440
48 REG_MSG_SEPARATOR_PLATE_MATRIX INT16 隔膜矩阵点位 hold_register 17450
49 REG_MSG_TIP_BOX_MATRIX INT16 移液枪头矩阵点位 hold_register 17480
50 REG_MSG_NE_PLATE_NUM INT16 负极片盘数 hold_register 17443
51 REG_MSG_SEPARATOR_PLATE_NUM INT16 隔膜盘数 hold_register 17453
52 REG_MSG_PRESS_MODE BOOL 压制模式(false:压力检测模式,True:距离模式) coil 9360 电池压制模式
53
54 BOOL 视觉对位(false:使用,true:忽略) coil 9300 视觉对位
55 BOOL 复检(false:使用,true:忽略) coil 9310 视觉复检
56 BOOL 手套箱_左仓(false:使用,true:忽略) coil 9320 手套箱左仓
57 BOOL 手套箱_右仓(false:使用,true:忽略) coil 9420 手套箱右仓
58 BOOL 真空检知(false:使用,true:忽略) coil 9350 真空检知
59 BOOL 电解液添加模式(false:单次滴液,true:二次滴液) coil 9370 滴液模式
60 BOOL 正极片称重(false:使用,true:忽略) coil 9380 正极片称重
61 BOOL 正负极片组装方式(false:正装,true:倒装) coil 9390 正负极反装
62 BOOL 压制清洁(false:使用,true:忽略) coil 9400 压制清洁
63 BOOL 物料盘摆盘方式(false:水平摆盘,true:堆叠摆盘) coil 9410 负极片摆盘方式
64 REG_MSG_BATTERY_CLEAN_IGNORE BOOL 忽略电池清洁(false:使用,true:忽略) coil 9460

View File

@@ -1,64 +0,0 @@
Name,DataType,InitValue,Comment,Attribute,DeviceType,Address,
COIL_SYS_START_CMD,BOOL,,,,coil,8010,
COIL_SYS_STOP_CMD,BOOL,,,,coil,8020,
COIL_SYS_RESET_CMD,BOOL,,,,coil,8030,
COIL_SYS_HAND_CMD,BOOL,,,,coil,8040,
COIL_SYS_AUTO_CMD,BOOL,,,,coil,8050,
COIL_SYS_INIT_CMD,BOOL,,,,coil,8060,
COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,,,,coil,8700,
COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,,,,coil,8710,unilab_rec_msg_succ_cmd
COIL_SYS_START_STATUS,BOOL,,,,coil,8210,
COIL_SYS_STOP_STATUS,BOOL,,,,coil,8220,
COIL_SYS_RESET_STATUS,BOOL,,,,coil,8230,
COIL_SYS_HAND_STATUS,BOOL,,,,coil,8240,
COIL_SYS_AUTO_STATUS,BOOL,,,,coil,8250,
COIL_SYS_INIT_STATUS,BOOL,,,,coil,8260,
COIL_REQUEST_REC_MSG_STATUS,BOOL,,,,coil,8500,
COIL_REQUEST_SEND_MSG_STATUS,BOOL,,,,coil,8510,request_send_msg_status
REG_MSG_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,11000,
REG_MSG_ELECTROLYTE_NUM,INT16,,,,hold_register,11002,unilab_send_msg_electrolyte_num
REG_MSG_ELECTROLYTE_VOLUME,INT16,,,,hold_register,11004,unilab_send_msg_electrolyte_vol
REG_MSG_ASSEMBLY_TYPE,INT16,,,,hold_register,11006,unilab_send_msg_assembly_type
REG_MSG_ASSEMBLY_PRESSURE,INT16,,,,hold_register,11008,unilab_send_msg_assembly_pressure
REG_DATA_ASSEMBLY_COIN_CELL_NUM,INT16,,,,hold_register,10000,data_assembly_coin_cell_num
REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,,,,hold_register,10002,data_open_circuit_voltage
REG_DATA_AXIS_X_POS,FLOAT32,,,,hold_register,10004,
REG_DATA_AXIS_Y_POS,FLOAT32,,,,hold_register,10006,
REG_DATA_AXIS_Z_POS,FLOAT32,,,,hold_register,10008,
REG_DATA_POLE_WEIGHT,FLOAT32,,,,hold_register,10010,data_pole_weight
REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,,,,hold_register,10012,data_assembly_time
REG_DATA_ASSEMBLY_PRESSURE,INT16,,,,hold_register,10014,data_assembly_pressure
REG_DATA_ELECTROLYTE_VOLUME,INT16,,,,hold_register,10016,data_electrolyte_volume
REG_DATA_COIN_NUM,INT16,,,,hold_register,10018,data_coin_num
REG_DATA_ELECTROLYTE_CODE,STRING,,,,hold_register,10020,data_electrolyte_code()
REG_DATA_COIN_CELL_CODE,STRING,,,,hold_register,10030,data_coin_cell_code()
REG_DATA_STACK_VISON_CODE,STRING,,,,hold_register,12004,data_stack_vision_code()
REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,,,,hold_register,10050,data_glove_box_pressure
REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,,,,hold_register,10052,data_glove_box_water_content
REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,,,hold_register,10054,data_glove_box_o2_content
UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8720,
UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8520,
REG_MSG_ELECTROLYTE_NUM_USED,INT16,,,,hold_register,496,
REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,10000,
UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,8730,
UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,8530,
REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,10018,ASSEMBLY_TYPE7or8
COIL_ALUMINUM_FOIL,BOOL,,使用铝箔垫,,coil,8340,
REG_MSG_NE_PLATE_MATRIX,INT16,,负极片矩阵点位,,hold_register,440,
REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,隔膜矩阵点位,,hold_register,450,
REG_MSG_TIP_BOX_MATRIX,INT16,,移液枪头矩阵点位,,hold_register,480,
REG_MSG_NE_PLATE_NUM,INT16,,负极片盘数,,hold_register,443,
REG_MSG_SEPARATOR_PLATE_NUM,INT16,,隔膜盘数,,hold_register,453,
REG_MSG_PRESS_MODE,BOOL,,压制模式false:压力检测模式True:距离模式),,coil,8360,电池压制模式
,,,,,,,
,BOOL,,视觉对位false:使用true:忽略),,coil,8300,视觉对位
,BOOL,,复检false:使用true:忽略),,coil,8310,视觉复检
,BOOL,,手套箱_左仓false:使用true:忽略),,coil,8320,手套箱左仓
,BOOL,,手套箱_右仓false:使用true:忽略),,coil,8420,手套箱右仓
,BOOL,,真空检知false:使用true:忽略),,coil,8350,真空检知
,BOOL,,电解液添加模式false:单次滴液true:二次滴液),,coil,8370,滴液模式
,BOOL,,正极片称重false:使用true:忽略),,coil,8380,正极片称重
,BOOL,,正负极片组装方式false:正装true:倒装),,coil,8390,正负极反装
,BOOL,,压制清洁false:使用true:忽略),,coil,8400,压制清洁
,BOOL,,物料盘摆盘方式false:水平摆盘true:堆叠摆盘),,coil,8410,负极片摆盘方式
REG_MSG_BATTERY_CLEAN_IGNORE,BOOL,,忽略电池清洁false:使用true:忽略),,coil,8460,
1 Name DataType InitValue Comment Attribute DeviceType Address
2 COIL_SYS_START_CMD BOOL coil 8010
3 COIL_SYS_STOP_CMD BOOL coil 8020
4 COIL_SYS_RESET_CMD BOOL coil 8030
5 COIL_SYS_HAND_CMD BOOL coil 8040
6 COIL_SYS_AUTO_CMD BOOL coil 8050
7 COIL_SYS_INIT_CMD BOOL coil 8060
8 COIL_UNILAB_SEND_MSG_SUCC_CMD BOOL coil 8700
9 COIL_UNILAB_REC_MSG_SUCC_CMD BOOL coil 8710 unilab_rec_msg_succ_cmd
10 COIL_SYS_START_STATUS BOOL coil 8210
11 COIL_SYS_STOP_STATUS BOOL coil 8220
12 COIL_SYS_RESET_STATUS BOOL coil 8230
13 COIL_SYS_HAND_STATUS BOOL coil 8240
14 COIL_SYS_AUTO_STATUS BOOL coil 8250
15 COIL_SYS_INIT_STATUS BOOL coil 8260
16 COIL_REQUEST_REC_MSG_STATUS BOOL coil 8500
17 COIL_REQUEST_SEND_MSG_STATUS BOOL coil 8510 request_send_msg_status
18 REG_MSG_ELECTROLYTE_USE_NUM INT16 hold_register 11000
19 REG_MSG_ELECTROLYTE_NUM INT16 hold_register 11002 unilab_send_msg_electrolyte_num
20 REG_MSG_ELECTROLYTE_VOLUME INT16 hold_register 11004 unilab_send_msg_electrolyte_vol
21 REG_MSG_ASSEMBLY_TYPE INT16 hold_register 11006 unilab_send_msg_assembly_type
22 REG_MSG_ASSEMBLY_PRESSURE INT16 hold_register 11008 unilab_send_msg_assembly_pressure
23 REG_DATA_ASSEMBLY_COIN_CELL_NUM INT16 hold_register 10000 data_assembly_coin_cell_num
24 REG_DATA_OPEN_CIRCUIT_VOLTAGE FLOAT32 hold_register 10002 data_open_circuit_voltage
25 REG_DATA_AXIS_X_POS FLOAT32 hold_register 10004
26 REG_DATA_AXIS_Y_POS FLOAT32 hold_register 10006
27 REG_DATA_AXIS_Z_POS FLOAT32 hold_register 10008
28 REG_DATA_POLE_WEIGHT FLOAT32 hold_register 10010 data_pole_weight
29 REG_DATA_ASSEMBLY_PER_TIME FLOAT32 hold_register 10012 data_assembly_time
30 REG_DATA_ASSEMBLY_PRESSURE INT16 hold_register 10014 data_assembly_pressure
31 REG_DATA_ELECTROLYTE_VOLUME INT16 hold_register 10016 data_electrolyte_volume
32 REG_DATA_COIN_NUM INT16 hold_register 10018 data_coin_num
33 REG_DATA_ELECTROLYTE_CODE STRING hold_register 10020 data_electrolyte_code()
34 REG_DATA_COIN_CELL_CODE STRING hold_register 10030 data_coin_cell_code()
35 REG_DATA_STACK_VISON_CODE STRING hold_register 12004 data_stack_vision_code()
36 REG_DATA_GLOVE_BOX_PRESSURE FLOAT32 hold_register 10050 data_glove_box_pressure
37 REG_DATA_GLOVE_BOX_WATER_CONTENT FLOAT32 hold_register 10052 data_glove_box_water_content
38 REG_DATA_GLOVE_BOX_O2_CONTENT FLOAT32 hold_register 10054 data_glove_box_o2_content
39 UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM BOOL coil 8720
40 UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM BOOL coil 8520
41 REG_MSG_ELECTROLYTE_NUM_USED INT16 hold_register 496
42 REG_DATA_ELECTROLYTE_USE_NUM INT16 hold_register 10000
43 UNILAB_SEND_FINISHED_CMD BOOL coil 8730
44 UNILAB_RECE_FINISHED_CMD BOOL coil 8530
45 REG_DATA_ASSEMBLY_TYPE INT16 hold_register 10018 ASSEMBLY_TYPE7or8
46 COIL_ALUMINUM_FOIL BOOL 使用铝箔垫 coil 8340
47 REG_MSG_NE_PLATE_MATRIX INT16 负极片矩阵点位 hold_register 440
48 REG_MSG_SEPARATOR_PLATE_MATRIX INT16 隔膜矩阵点位 hold_register 450
49 REG_MSG_TIP_BOX_MATRIX INT16 移液枪头矩阵点位 hold_register 480
50 REG_MSG_NE_PLATE_NUM INT16 负极片盘数 hold_register 443
51 REG_MSG_SEPARATOR_PLATE_NUM INT16 隔膜盘数 hold_register 453
52 REG_MSG_PRESS_MODE BOOL 压制模式(false:压力检测模式,True:距离模式) coil 8360 电池压制模式
53
54 BOOL 视觉对位(false:使用,true:忽略) coil 8300 视觉对位
55 BOOL 复检(false:使用,true:忽略) coil 8310 视觉复检
56 BOOL 手套箱_左仓(false:使用,true:忽略) coil 8320 手套箱左仓
57 BOOL 手套箱_右仓(false:使用,true:忽略) coil 8420 手套箱右仓
58 BOOL 真空检知(false:使用,true:忽略) coil 8350 真空检知
59 BOOL 电解液添加模式(false:单次滴液,true:二次滴液) coil 8370 滴液模式
60 BOOL 正极片称重(false:使用,true:忽略) coil 8380 正极片称重
61 BOOL 正负极片组装方式(false:正装,true:倒装) coil 8390 正负极反装
62 BOOL 压制清洁(false:使用,true:忽略) coil 8400 压制清洁
63 BOOL 物料盘摆盘方式(false:水平摆盘,true:堆叠摆盘) coil 8410 负极片摆盘方式
64 REG_MSG_BATTERY_CLEAN_IGNORE BOOL 忽略电池清洁(false:使用,true:忽略) coil 8460

View File

@@ -1,39 +0,0 @@
{
"nodes": [
{
"id": "bioyond_cell_workstation",
"name": "配液分液工站",
"children": [
],
"parent": null,
"type": "device",
"class": "bioyond_cell",
"config": {
"protocol_type": [],
"station_resource": {}
},
"data": {}
},
{
"id": "BatteryStation",
"name": "扣电工作站",
"children": [
"coin_cell_deck"
],
"parent": null,
"type": "device",
"class": "coincellassemblyworkstation_device",
"position": {
"x": -600,
"y": -400,
"z": 0
},
"config": {
"debug_mode": false,
"protocol_type": []
}
}
],
"links": []
}

View File

@@ -1,23 +1,8 @@
{ {
"nodes": [ "nodes": [
{
"id": "bioyond_cell_workstation",
"name": "配液分液工站",
"children": [
],
"parent": null,
"type": "device",
"class": "bioyond_cell",
"config": {
"protocol_type": [],
"station_resource": {}
},
"data": {}
},
{ {
"id": "BatteryStation", "id": "BatteryStation",
"name": "扣电组装工作站", "name": "扣电工作站",
"children": [ "children": [
"coin_cell_deck" "coin_cell_deck"
], ],

View File

@@ -171,6 +171,7 @@ class WorkstationBase(ABC):
def post_init(self, ros_node: ROS2WorkstationNode) -> None: def post_init(self, ros_node: ROS2WorkstationNode) -> None:
# 初始化物料系统 # 初始化物料系统
self._ros_node = ros_node self._ros_node = ros_node
self._ros_node.update_resource([self.deck])
def _build_resource_mappings(self, deck: Deck): def _build_resource_mappings(self, deck: Deck):
"""递归构建资源映射""" """递归构建资源映射"""

File diff suppressed because it is too large Load Diff

View File

@@ -1,3 +1,231 @@
hplc.agilent:
category:
- characterization_chromatic
class:
action_value_mappings:
auto-check_status:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 检查安捷伦HPLC设备状态的函数。用于监控设备的运行状态、连接状态、错误信息等关键指标。该函数定期查询设备状态确保系统稳定运行及时发现和报告设备异常。适用于自动化流程中的设备监控、故障诊断、系统维护等场景。
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: check_status参数
type: object
type: UniLabJsonCommand
auto-extract_data_from_txt:
feedback: {}
goal: {}
goal_default:
file_path: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 从文本文件中提取分析数据的函数。用于解析安捷伦HPLC生成的结果文件提取峰面积、保留时间、浓度等关键分析数据。支持多种文件格式的自动识别和数据结构化处理为后续数据分析和报告生成提供标准化的数据格式。适用于批量数据处理、结果验证、质量控制等分析工作流程。
properties:
feedback: {}
goal:
properties:
file_path:
type: string
required:
- file_path
type: object
result: {}
required:
- goal
title: extract_data_from_txt参数
type: object
type: UniLabJsonCommand
auto-start_sequence:
feedback: {}
goal: {}
goal_default:
params: null
resource: null
wf_name: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 启动安捷伦HPLC分析序列的函数。用于执行预定义的分析方法序列包括样品进样、色谱分离、检测等完整的分析流程。支持参数配置、资源分配、工作流程管理等功能实现全自动的样品分析。适用于批量样品处理、标准化分析、质量检测等需要连续自动分析的应用场景。
properties:
feedback: {}
goal:
properties:
params:
type: string
resource:
type: object
wf_name:
type: string
required:
- wf_name
type: object
result: {}
required:
- goal
title: start_sequence参数
type: object
type: UniLabJsonCommand
auto-try_close_sub_device:
feedback: {}
goal: {}
goal_default:
device_name: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 尝试关闭HPLC子设备的函数。用于安全地关闭泵、检测器、进样器等各个子模块确保设备正常断开连接并保护硬件安全。该函数提供错误处理和状态确认机制避免强制关闭可能造成的设备损坏。适用于设备维护、系统重启、紧急停机等需要安全关闭设备的场景。
properties:
feedback: {}
goal:
properties:
device_name:
type: string
required: []
type: object
result: {}
required:
- goal
title: try_close_sub_device参数
type: object
type: UniLabJsonCommand
auto-try_open_sub_device:
feedback: {}
goal: {}
goal_default:
device_name: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 尝试打开HPLC子设备的函数。用于初始化和连接泵、检测器、进样器等各个子模块建立设备通信并进行自检。该函数提供连接验证和错误恢复机制确保子设备正常启动并准备就绪。适用于设备初始化、系统启动、设备重连等需要建立设备连接的场景。
properties:
feedback: {}
goal:
properties:
device_name:
type: string
required: []
type: object
result: {}
required:
- goal
title: try_open_sub_device参数
type: object
type: UniLabJsonCommand
execute_command_from_outer:
feedback: {}
goal:
command: command
goal_default:
command: ''
handles: {}
result:
success: success
schema:
description: ''
properties:
feedback:
properties:
status:
type: string
required:
- status
title: SendCmd_Feedback
type: object
goal:
properties:
command:
type: string
required:
- command
title: SendCmd_Goal
type: object
result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: SendCmd_Result
type: object
required:
- goal
title: SendCmd
type: object
type: SendCmd
module: unilabos.devices.hplc.AgilentHPLC:HPLCDriver
status_types:
could_run: bool
data_file: String
device_status: str
driver_init_ok: bool
finish_status: str
is_running: bool
status_text: str
success: bool
type: python
config_info: []
description: 安捷伦高效液相色谱HPLC分析设备用于复杂化合物的分离、检测和定量分析。该设备通过UI自动化技术控制安捷伦ChemStation软件实现全自动的样品分析流程。具备序列启动、设备状态监控、数据文件提取、结果处理等功能。支持多样品批量处理和实时状态反馈适用于药物分析、环境检测、食品安全、化学研究等需要高精度色谱分析的实验室应用。
handles: []
icon: ''
init_param_schema:
config:
properties:
driver_debug:
default: false
type: string
required: []
type: object
data:
properties:
could_run:
type: boolean
data_file:
items:
type: string
type: array
device_status:
type: string
driver_init_ok:
type: boolean
finish_status:
type: string
is_running:
type: boolean
status_text:
type: string
success:
type: boolean
required:
- status_text
- device_status
- could_run
- driver_init_ok
- is_running
- success
- finish_status
- data_file
type: object
version: 1.0.0
hplc.agilent-zhida: hplc.agilent-zhida:
category: category:
- characterization_chromatic - characterization_chromatic

View File

@@ -1 +1,194 @@
{} raman.home_made:
category:
- characterization_optic
class:
action_value_mappings:
auto-ccd_time:
feedback: {}
goal: {}
goal_default:
int_time: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 设置CCD检测器积分时间的函数。用于配置拉曼光谱仪的信号采集时间控制光谱数据的质量和信噪比。较长的积分时间可获得更高的信号强度和更好的光谱质量但会增加测量时间。该函数允许根据样品特性和测量要求动态调整检测参数优化测量效果。
properties:
feedback: {}
goal:
properties:
int_time:
type: string
required:
- int_time
type: object
result: {}
required:
- goal
title: ccd_time参数
type: object
type: UniLabJsonCommand
auto-laser_on_power:
feedback: {}
goal: {}
goal_default:
output_voltage_laser: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 设置激光器输出功率的函数。用于控制拉曼光谱仪激光器的功率输出,调节激光强度以适应不同样品的测量需求。适当的激光功率能够获得良好的拉曼信号同时避免样品损伤。该函数支持精确的功率控制,确保测量结果的稳定性和重现性。
properties:
feedback: {}
goal:
properties:
output_voltage_laser:
type: string
required:
- output_voltage_laser
type: object
result: {}
required:
- goal
title: laser_on_power参数
type: object
type: UniLabJsonCommand
auto-raman_without_background:
feedback: {}
goal: {}
goal_default:
int_time: null
laser_power: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 执行无背景扣除的拉曼光谱测量函数。用于直接采集样品的拉曼光谱信号,不进行背景校正处理。该函数配置积分时间和激光功率参数,获取原始光谱数据用于后续的数据处理分析。适用于对光谱数据质量要求较高或需要自定义背景处理流程的测量场景。
properties:
feedback: {}
goal:
properties:
int_time:
type: string
laser_power:
type: string
required:
- int_time
- laser_power
type: object
result: {}
required:
- goal
title: raman_without_background参数
type: object
type: UniLabJsonCommand
auto-raman_without_background_average:
feedback: {}
goal: {}
goal_default:
average: null
int_time: null
laser_power: null
sample_name: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 执行多次平均的无背景拉曼光谱测量函数。通过多次测量取平均值来提高光谱数据的信噪比和测量精度,减少随机噪声影响。该函数支持自定义平均次数、积分时间、激光功率等参数,并可为样品指定名称便于数据管理。适用于对测量精度要求较高的定量分析和研究应用。
properties:
feedback: {}
goal:
properties:
average:
type: string
int_time:
type: string
laser_power:
type: string
sample_name:
type: string
required:
- sample_name
- int_time
- laser_power
- average
type: object
result: {}
required:
- goal
title: raman_without_background_average参数
type: object
type: UniLabJsonCommand
raman_cmd:
feedback: {}
goal:
command: command
goal_default:
command: ''
handles: {}
result:
success: success
schema:
description: ''
properties:
feedback:
properties:
status:
type: string
required:
- status
title: SendCmd_Feedback
type: object
goal:
properties:
command:
type: string
required:
- command
title: SendCmd_Goal
type: object
result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: SendCmd_Result
type: object
required:
- goal
title: SendCmd
type: object
type: SendCmd
module: unilabos.devices.raman_uv.home_made_raman:RamanObj
status_types: {}
type: python
config_info: []
description: 拉曼光谱分析设备用于物质的分子结构和化学成分表征。该设备集成激光器和CCD检测器通过串口通信控制激光功率和光谱采集。具备背景扣除、多次平均、自动数据处理等功能支持高精度的拉曼光谱测量。适用于材料表征、化学分析、质量控制、研究开发等需要分子指纹识别和结构分析的实验应用。
handles: []
icon: ''
init_param_schema:
config:
properties:
baudrate_ccd:
default: 921600
type: string
baudrate_laser:
default: 9600
type: string
port_ccd:
type: string
port_laser:
type: string
required:
- port_laser
- port_ccd
type: object
data:
properties: {}
required: []
type: object
version: 1.0.0

View File

@@ -1,584 +0,0 @@
coincellassemblyworkstation_device:
category:
- coin_cell_workstation
class:
action_value_mappings:
auto-change_hole_sheet_to_2:
feedback: {}
goal: {}
goal_default:
hole: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
hole:
type: object
required:
- hole
type: object
result: {}
required:
- goal
title: change_hole_sheet_to_2参数
type: object
type: UniLabJsonCommandAsync
auto-fill_plate:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: fill_plate参数
type: object
type: UniLabJsonCommandAsync
auto-fun_wuliao_test:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: fun_wuliao_test参数
type: object
type: UniLabJsonCommand
auto-func_allpack_cmd:
feedback: {}
goal: {}
goal_default:
assembly_pressure: 4200
assembly_type: 7
elec_num: null
elec_use_num: null
elec_vol: 50
file_path: C:\Users\67484\Desktop
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
assembly_pressure:
default: 4200
type: integer
assembly_type:
default: 7
type: integer
elec_num:
type: string
elec_use_num:
type: string
elec_vol:
default: 50
type: integer
file_path:
default: C:\Users\67484\Desktop
type: string
required:
- elec_num
- elec_use_num
type: object
result: {}
required:
- goal
title: func_allpack_cmd参数
type: object
type: UniLabJsonCommand
auto-func_get_csv_export_status:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_get_csv_export_status参数
type: object
type: UniLabJsonCommand
auto-func_pack_device_auto:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_device_auto参数
type: object
type: UniLabJsonCommand
auto-func_pack_device_init:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_device_init参数
type: object
type: UniLabJsonCommand
auto-func_pack_device_start:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_device_start参数
type: object
type: UniLabJsonCommand
auto-func_pack_device_stop:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_device_stop参数
type: object
type: UniLabJsonCommand
auto-func_pack_get_msg_cmd:
feedback: {}
goal: {}
goal_default:
file_path: D:\coin_cell_data
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
file_path:
default: D:\coin_cell_data
type: string
required: []
type: object
result: {}
required:
- goal
title: func_pack_get_msg_cmd参数
type: object
type: UniLabJsonCommand
auto-func_pack_send_bottle_num:
feedback: {}
goal: {}
goal_default:
bottle_num: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
bottle_num:
type: string
required:
- bottle_num
type: object
result: {}
required:
- goal
title: func_pack_send_bottle_num参数
type: object
type: UniLabJsonCommand
auto-func_pack_send_finished_cmd:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_pack_send_finished_cmd参数
type: object
type: UniLabJsonCommand
auto-func_pack_send_msg_cmd:
feedback: {}
goal: {}
goal_default:
assembly_pressure: null
assembly_type: null
elec_use_num: null
elec_vol: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
assembly_pressure:
type: string
assembly_type:
type: string
elec_use_num:
type: string
elec_vol:
type: string
required:
- elec_use_num
- elec_vol
- assembly_type
- assembly_pressure
type: object
result: {}
required:
- goal
title: func_pack_send_msg_cmd参数
type: object
type: UniLabJsonCommand
auto-func_read_data_and_output:
feedback: {}
goal: {}
goal_default:
file_path: D:\coin_cell_data
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
file_path:
default: D:\coin_cell_data
type: string
required: []
type: object
result: {}
required:
- goal
title: func_read_data_and_output参数
type: object
type: UniLabJsonCommand
auto-func_stop_read_data:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: func_stop_read_data参数
type: object
type: UniLabJsonCommand
auto-modify_deck_name:
feedback: {}
goal: {}
goal_default:
resource_name: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
resource_name:
type: string
required:
- resource_name
type: object
result: {}
required:
- goal
title: modify_deck_name参数
type: object
type: UniLabJsonCommand
auto-post_init:
feedback: {}
goal: {}
goal_default:
ros_node: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
ros_node:
type: object
required:
- ros_node
type: object
result: {}
required:
- goal
title: post_init参数
type: object
type: UniLabJsonCommand
auto-qiming_coin_cell_code:
feedback: {}
goal: {}
goal_default:
battery_clean_ignore: false
battery_pressure: 4000
battery_pressure_mode: true
fujipian_juzhendianwei: 0
fujipian_panshu: null
gemo_juzhendianwei: 0
gemopanshu: 0
lvbodian: true
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
battery_clean_ignore:
default: false
type: boolean
battery_pressure:
default: 4000
type: integer
battery_pressure_mode:
default: true
type: boolean
fujipian_juzhendianwei:
default: 0
type: integer
fujipian_panshu:
type: integer
gemo_juzhendianwei:
default: 0
type: integer
gemopanshu:
default: 0
type: integer
lvbodian:
default: true
type: boolean
required:
- fujipian_panshu
type: object
result: {}
required:
- goal
title: qiming_coin_cell_code参数
type: object
type: UniLabJsonCommand
module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation
status_types:
data_assembly_coin_cell_num: int
data_assembly_pressure: int
data_assembly_time: float
data_axis_x_pos: float
data_axis_y_pos: float
data_axis_z_pos: float
data_coin_cell_code: str
data_coin_num: int
data_electrolyte_code: str
data_electrolyte_volume: int
data_glove_box_o2_content: float
data_glove_box_pressure: float
data_glove_box_water_content: float
data_open_circuit_voltage: float
data_pole_weight: float
request_rec_msg_status: bool
request_send_msg_status: bool
sys_mode: str
sys_status: str
type: python
config_info: []
description: ''
handles: []
icon: coin_cell_assembly_picture.webp
init_param_schema:
config:
properties:
address:
default: 172.21.32.111
type: string
debug_mode:
default: false
type: boolean
deck:
type: object
port:
default: '502'
type: string
required: []
type: object
data:
properties:
data_assembly_coin_cell_num:
type: integer
data_assembly_pressure:
type: integer
data_assembly_time:
type: number
data_axis_x_pos:
type: number
data_axis_y_pos:
type: number
data_axis_z_pos:
type: number
data_coin_cell_code:
type: string
data_coin_num:
type: integer
data_electrolyte_code:
type: string
data_electrolyte_volume:
type: integer
data_glove_box_o2_content:
type: number
data_glove_box_pressure:
type: number
data_glove_box_water_content:
type: number
data_open_circuit_voltage:
type: number
data_pole_weight:
type: number
request_rec_msg_status:
type: boolean
request_send_msg_status:
type: boolean
sys_mode:
type: string
sys_status:
type: string
required:
- sys_status
- sys_mode
- request_rec_msg_status
- request_send_msg_status
- data_assembly_coin_cell_num
- data_assembly_time
- data_open_circuit_voltage
- data_axis_x_pos
- data_axis_y_pos
- data_axis_z_pos
- data_pole_weight
- data_assembly_pressure
- data_electrolyte_volume
- data_coin_num
- data_coin_cell_code
- data_electrolyte_code
- data_glove_box_pressure
- data_glove_box_o2_content
- data_glove_box_water_content
type: object
registry_type: device
version: 1.0.0

File diff suppressed because it is too large Load Diff

View File

@@ -1361,7 +1361,8 @@ laiyu_liquid:
mix_liquid_height: 0.0 mix_liquid_height: 0.0
mix_rate: 0 mix_rate: 0
mix_stage: '' mix_stage: ''
mix_times: 0 mix_times:
- 0
mix_vol: 0 mix_vol: 0
none_keys: none_keys:
- '' - ''
@@ -1491,9 +1492,11 @@ laiyu_liquid:
mix_stage: mix_stage:
type: string type: string
mix_times: mix_times:
maximum: 2147483647 items:
minimum: -2147483648 maximum: 2147483647
type: integer minimum: -2147483648
type: integer
type: array
mix_vol: mix_vol:
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648

View File

@@ -3994,7 +3994,8 @@ liquid_handler:
mix_liquid_height: 0.0 mix_liquid_height: 0.0
mix_rate: 0 mix_rate: 0
mix_stage: '' mix_stage: ''
mix_times: 0 mix_times:
- 0
mix_vol: 0 mix_vol: 0
none_keys: none_keys:
- '' - ''
@@ -4150,9 +4151,11 @@ liquid_handler:
mix_stage: mix_stage:
type: string type: string
mix_times: mix_times:
maximum: 2147483647 items:
minimum: -2147483648 maximum: 2147483647
type: integer minimum: -2147483648
type: integer
type: array
mix_vol: mix_vol:
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648
@@ -5012,7 +5015,8 @@ liquid_handler.biomek:
mix_liquid_height: 0.0 mix_liquid_height: 0.0
mix_rate: 0 mix_rate: 0
mix_stage: '' mix_stage: ''
mix_times: 0 mix_times:
- 0
mix_vol: 0 mix_vol: 0
none_keys: none_keys:
- '' - ''
@@ -5155,9 +5159,11 @@ liquid_handler.biomek:
mix_stage: mix_stage:
type: string type: string
mix_times: mix_times:
maximum: 2147483647 items:
minimum: -2147483648 maximum: 2147483647
type: integer minimum: -2147483648
type: integer
type: array
mix_vol: mix_vol:
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648
@@ -7801,7 +7807,8 @@ liquid_handler.prcxi:
mix_liquid_height: 0.0 mix_liquid_height: 0.0
mix_rate: 0 mix_rate: 0
mix_stage: '' mix_stage: ''
mix_times: 0 mix_times:
- 0
mix_vol: 0 mix_vol: 0
none_keys: none_keys:
- '' - ''
@@ -7930,9 +7937,11 @@ liquid_handler.prcxi:
mix_stage: mix_stage:
type: string type: string
mix_times: mix_times:
maximum: 2147483647 items:
minimum: -2147483648 maximum: 2147483647
type: integer minimum: -2147483648
type: integer
type: array
mix_vol: mix_vol:
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: -2147483648

File diff suppressed because it is too large Load Diff

View File

@@ -834,3 +834,174 @@ linear_motion.toyo_xyz.sim:
mesh: toyo_xyz mesh: toyo_xyz
type: device type: device
version: 1.0.0 version: 1.0.0
motor.iCL42:
category:
- robot_linear_motion
class:
action_value_mappings:
auto-execute_run_motor:
feedback: {}
goal: {}
goal_default:
mode: null
position: null
velocity: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 步进电机执行运动函数。直接执行电机运动命令,包括位置设定、速度控制和路径规划。该函数处理底层的电机控制协议,消除警告信息,设置运动参数并启动电机运行。适用于需要直接控制电机运动的应用场景。
properties:
feedback: {}
goal:
properties:
mode:
type: string
position:
type: number
velocity:
type: integer
required:
- mode
- position
- velocity
type: object
result: {}
required:
- goal
title: execute_run_motor参数
type: object
type: UniLabJsonCommand
auto-init_device:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: iCL42电机设备初始化函数。建立与iCL42步进电机驱动器的串口通信连接配置通信参数包括波特率、数据位、校验位等。该函数是电机使用前的必要步骤确保驱动器处于可控状态并准备接收运动指令。
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: init_device参数
type: object
type: UniLabJsonCommand
auto-run_motor:
feedback: {}
goal: {}
goal_default:
mode: null
position: null
velocity: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: 步进电机运动控制函数。根据指定的运动模式、目标位置和速度参数控制电机运动。支持多种运动模式和精确的位置控制,自动处理运动轨迹规划和执行。该函数提供异步执行和状态反馈,确保运动的准确性和可靠性。
properties:
feedback: {}
goal:
properties:
mode:
type: string
position:
type: number
velocity:
type: integer
required:
- mode
- position
- velocity
type: object
result: {}
required:
- goal
title: run_motor参数
type: object
type: UniLabJsonCommand
execute_command_from_outer:
feedback: {}
goal:
command: command
goal_default:
command: ''
handles: {}
result:
success: success
schema:
description: ''
properties:
feedback:
properties:
status:
type: string
required:
- status
title: SendCmd_Feedback
type: object
goal:
properties:
command:
type: string
required:
- command
title: SendCmd_Goal
type: object
result:
properties:
return_info:
type: string
success:
type: boolean
required:
- return_info
- success
title: SendCmd_Result
type: object
required:
- goal
title: SendCmd
type: object
type: SendCmd
module: unilabos.devices.motor.iCL42:iCL42Driver
status_types:
is_executing_run: bool
motor_position: int
success: bool
type: python
config_info: []
description: iCL42步进电机驱动器用于实验室设备的精密线性运动控制。该设备通过串口通信控制iCL42型步进电机驱动器支持多种运动模式和精确的位置、速度控制。具备位置反馈、运行状态监控和故障检测功能。适用于自动进样器、样品传送、精密定位平台等需要准确线性运动控制的实验室自动化设备。
handles: []
icon: ''
init_param_schema:
config:
properties:
device_address:
default: 1
type: integer
device_com:
default: COM9
type: string
required: []
type: object
data:
properties:
is_executing_run:
type: boolean
motor_position:
type: integer
success:
type: boolean
required:
- motor_position
- is_executing_run
- success
type: object
version: 1.0.0

File diff suppressed because it is too large Load Diff

View File

@@ -708,8 +708,6 @@ class Registry:
for status_name, status_type in device_config["class"]["status_types"].items(): for status_name, status_type in device_config["class"]["status_types"].items():
device_config["class"]["status_types"][status_name] = status_str_type_mapping[status_type] device_config["class"]["status_types"][status_name] = status_str_type_mapping[status_type]
for action_name, action_config in device_config["class"]["action_value_mappings"].items(): for action_name, action_config in device_config["class"]["action_value_mappings"].items():
if action_config["type"] not in action_str_type_mapping:
continue
action_config["type"] = action_str_type_mapping[action_config["type"]] action_config["type"] = action_str_type_mapping[action_config["type"]]
# 添加内置的驱动命令动作 # 添加内置的驱动命令动作
self._add_builtin_actions(device_config, device_id) self._add_builtin_actions(device_config, device_id)

View File

@@ -1,65 +0,0 @@
YB_qiang_tou:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_qiang_tou
type: pylabrobot
description: YB_qiang_tou
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_20ml_fenyeping:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_20ml_fenyeping
type: pylabrobot
description: YB_20ml_fenyeping
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_5ml_fenyeping:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_5ml_fenyeping
type: pylabrobot
description: YB_5ml_fenyeping
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_pei_ye_da_Bottle:
category:
- yb3
- YB_bottle
class:
module: unilabos.resources.bioyond.YB_bottles:YB_pei_ye_da_Bottle
type: pylabrobot
description: YB_pei_ye_da_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
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

View File

@@ -1,208 +0,0 @@
YB_100ml_yeti:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_100ml_yeti
type: pylabrobot
description: YB_100ml_yeti
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_1BottleCarrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_1BottleCarrier
type: pylabrobot
description: YB_1BottleCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_gaonianye:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_gaonianye
type: pylabrobot
description: YB_gaonianye
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_peiyepingdaban:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_peiyepingdaban
type: pylabrobot
description: YB_peiyepingdaban
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_6StockCarrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6StockCarrier
type: pylabrobot
description: YB_6StockCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_6VialCarrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_6VialCarrier
type: pylabrobot
description: YB_6VialCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_20ml_fenyepingban:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_20ml_fenyepingban
type: pylabrobot
description: YB_20ml_fenyepingban
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_5ml_fenyepingban:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_5ml_fenyepingban
type: pylabrobot
description: YB_5ml_fenyepingban
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
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
YB_shi_pei_qi_kuai:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_shi_pei_qi_kuai
type: pylabrobot
description: YB_shi_pei_qi_kuai
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_qiang_tou_he:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_qiang_tou_he
type: pylabrobot
description: YB_qiang_tou_he
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_gao_nian_ye_Bottle:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottles:YB_gao_nian_ye_Bottle
type: pylabrobot
description: YB_gao_nian_ye_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_jia_yang_tou_da:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottles:YB_jia_yang_tou_da
type: pylabrobot
description: YB_jia_yang_tou_da
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_jia_yang_tou_da_Carrier:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottle_carriers:YB_jia_yang_tou_da_Carrier
type: pylabrobot
description: YB_jia_yang_tou_da_Carrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_ye_100ml_Bottle:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottles:YB_ye_100ml_Bottle
type: pylabrobot
description: YB_ye_100ml_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
YB_ye_Bottle:
category:
- yb3
- YB_bottle_carriers
class:
module: unilabos.resources.bioyond.YB_bottles:YB_ye_Bottle
type: pylabrobot
description: YB_ye_Bottle
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -0,0 +1,48 @@
BIOYOND_PolymerStation_1BottleCarrier:
category:
- bottle_carriers
class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1BottleCarrier
type: pylabrobot
description: BIOYOND_PolymerStation_1BottleCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
BIOYOND_PolymerStation_1FlaskCarrier:
category:
- bottle_carriers
class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1FlaskCarrier
type: pylabrobot
description: BIOYOND_PolymerStation_1FlaskCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
BIOYOND_PolymerStation_6StockCarrier:
category:
- bottle_carriers
class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_6StockCarrier
type: pylabrobot
description: BIOYOND_PolymerStation_6StockCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0
BIOYOND_PolymerStation_6VialCarrier:
category:
- bottle_carriers
class:
module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_6VialCarrier
type: pylabrobot
description: BIOYOND_PolymerStation_6VialCarrier
handles: []
icon: ''
init_param_schema: {}
registry_type: resource
version: 1.0.0

View File

@@ -0,0 +1,50 @@
BIOYOND_PolymerStation_Liquid_Vial:
category:
- bottles
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Liquid_Vial
type: pylabrobot
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0
BIOYOND_PolymerStation_Reagent_Bottle:
category:
- bottles
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Reagent_Bottle
type: pylabrobot
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0
BIOYOND_PolymerStation_Solid_Stock:
category:
- bottles
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Solid_Stock
type: pylabrobot
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0
BIOYOND_PolymerStation_Solid_Vial:
category:
- bottles
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Solid_Vial
type: pylabrobot
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0
BIOYOND_PolymerStation_Solution_Beaker:
category:
- bottles
class:
module: unilabos.resources.bioyond.bottles:BIOYOND_PolymerStation_Solution_Beaker
type: pylabrobot
handles: []
icon: ''
init_param_schema: {}
version: 1.0.0

View File

@@ -22,15 +22,3 @@ BIOYOND_PolymerReactionStation_Deck:
init_param_schema: {} init_param_schema: {}
registry_type: resource registry_type: resource
version: 1.0.0 version: 1.0.0
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

View File

@@ -3,7 +3,7 @@ container:
- container - container
class: class:
module: unilabos.resources.container:RegularContainer module: unilabos.resources.container:RegularContainer
type: pylabrobot type: unilabos
description: regular organic container description: regular organic container
handles: handles:
- data_key: fluid_in - data_key: fluid_in

View File

@@ -1,653 +0,0 @@
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_jia_yang_tou_da,
YB_ye_Bottle,
YB_ye_100ml_Bottle,
YB_gao_nian_ye_Bottle,
YB_5ml_fenyeping,
YB_20ml_fenyeping,
YB_pei_ye_xiao_Bottle,
YB_pei_ye_da_Bottle,
YB_qiang_tou,
)
# 命名约定:试剂瓶-Bottle烧杯-Beaker烧瓶-Flask小瓶-Vial
def BIOYOND_Electrolyte_6VialCarrier(name: str) -> BottleCarrier:
"""6瓶载架 - 2x3布局"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 50.0
# 瓶位尺寸
bottle_diameter = 30.0
bottle_spacing_x = 42.0 # X方向间距
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=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="Electrolyte_6VialCarrier",
)
carrier.num_items_x = 3
carrier.num_items_y = 2
carrier.num_items_z = 1
# for i in range(6):
# carrier[i] = YB_Solid_Vial(f"{name}_vial_{i+1}")
return carrier
def BIOYOND_Electrolyte_1BottleCarrier(name: str) -> BottleCarrier:
"""1瓶载架 - 单个中央位置"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 100.0
# 烧杯尺寸
beaker_diameter = 80.0
# 计算中央位置
center_x = (carrier_size_x - beaker_diameter) / 2
center_y = (carrier_size_y - beaker_diameter) / 2
center_z = 5.0
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(center_x, center_y, center_z)],
resource_size_x=beaker_diameter,
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="Electrolyte_1BottleCarrier",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
# carrier[0] = YB_Solution_Beaker(f"{name}_beaker_1")
return carrier
def YB_6StockCarrier(name: str) -> BottleCarrier:
"""6瓶载架 - 2x3布局"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 50.0
# 瓶位尺寸
bottle_diameter = 20.0
bottle_spacing_x = 42.0 # X方向间距
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=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="6StockCarrier",
)
carrier.num_items_x = 3
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序
# for i in range(6):
# carrier[i] = YB_Solid_Stock(f"{name}_vial_{ordering[i]}")
return carrier
def YB_6VialCarrier(name: str) -> BottleCarrier:
"""6瓶载架 - 2x3布局"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 50.0
# 瓶位尺寸
bottle_diameter = 30.0
bottle_spacing_x = 42.0 # X方向间距
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=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="6VialCarrier",
)
carrier.num_items_x = 3
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序
# for i in range(3):
# carrier[i] = YB_Solid_Vial(f"{name}_solidvial_{ordering[i]}")
# for i in range(3, 6):
# carrier[i] = YB_Liquid_Vial(f"{name}_liquidvial_{ordering[i]}")
return carrier
# 1瓶载架 - 单个中央位置
def YB_1BottleCarrier(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 20.0
# 烧杯尺寸
beaker_diameter = 60.0
# 计算中央位置
center_x = (carrier_size_x - beaker_diameter) / 2
center_y = (carrier_size_y - beaker_diameter) / 2
center_z = 5.0
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(center_x, center_y, center_z)],
resource_size_x=beaker_diameter,
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="YB_1BottleCarrier",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = YB_ye_Bottle(f"{name}_flask_1")
return carrier
# 高粘液瓶载架 - 单个中央位置
def YB_gaonianye(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 20.0
# 烧杯尺寸
beaker_diameter = 60.0
# 计算中央位置
center_x = (carrier_size_x - beaker_diameter) / 2
center_y = (carrier_size_y - beaker_diameter) / 2
center_z = 5.0
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(center_x, center_y, center_z)],
resource_size_x=beaker_diameter,
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="YB_gaonianye",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = YB_gao_nian_ye_Bottle(f"{name}_flask_1")
return carrier
# 100ml液体瓶载架 - 单个中央位置
def YB_100ml_yeti(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 20.0
# 烧杯尺寸
beaker_diameter = 60.0
# 计算中央位置
center_x = (carrier_size_x - beaker_diameter) / 2
center_y = (carrier_size_y - beaker_diameter) / 2
center_z = 5.0
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(center_x, center_y, center_z)],
resource_size_x=beaker_diameter,
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="YB_100ml_yeti",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = YB_ye_100ml_Bottle(f"{name}_flask_1")
return carrier
# 5ml分液瓶板 - 4x2布局8个位置
def YB_5ml_fenyepingban(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 50.0
# 瓶位尺寸
bottle_diameter = 15.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
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=4,
num_items_y=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_5ml_fenyepingban",
)
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_5ml_fenyeping(f"{name}_vial_{ordering[i]}")
return carrier
# 20ml分液瓶板 - 4x2布局8个位置
def YB_20ml_fenyepingban(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 70.0
# 瓶位尺寸
bottle_diameter = 20.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
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=4,
num_items_y=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_20ml_fenyepingban",
)
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_20ml_fenyeping(f"{name}_vial_{ordering[i]}")
return carrier
# 配液瓶(小)板 - 4x2布局8个位置
def YB_peiyepingxiaoban(name: str) -> BottleCarrier:
# 载架尺寸 (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
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=4,
num_items_y=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
# 配液瓶(大)板 - 2x2布局4个位置
def YB_peiyepingdaban(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 95.0
# 瓶位尺寸
bottle_diameter = 55.0
bottle_spacing_x = 60.0 # X方向间距
bottle_spacing_y = 60.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (2 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=2,
num_items_y=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_peiyepingdaban",
)
carrier.num_items_x = 2
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "B1", "B2"]
for i in range(4):
carrier[i] = YB_pei_ye_da_Bottle(f"{name}_bottle_{ordering[i]}")
return carrier
# 加样头(大)板 - 1x1布局1个位置
def YB_jia_yang_tou_da_Carrier(name: str) -> BottleCarrier:
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 95.0
# 瓶位尺寸
bottle_diameter = 35.0
bottle_spacing_x = 42.0 # X方向间距
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (1 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (1 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=1,
num_items_y=1,
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_jia_yang_tou_da_Carrier",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = YB_jia_yang_tou_da(f"{name}_head_1")
return carrier
def YB_shi_pei_qi_kuai(name: str) -> BottleCarrier:
"""适配器块 - 单个中央位置"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 30.0
# 适配器尺寸
adapter_diameter = 80.0
# 计算中央位置
center_x = (carrier_size_x - adapter_diameter) / 2
center_y = (carrier_size_y - adapter_diameter) / 2
center_z = 0.0
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(center_x, center_y, center_z)],
resource_size_x=adapter_diameter,
resource_size_y=adapter_diameter,
name_prefix=name,
),
model="YB_shi_pei_qi_kuai",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
# 适配器块本身不包含瓶子,只是一个支撑结构
return carrier
def YB_qiang_tou_he(name: str) -> BottleCarrier:
"""枪头盒 - 8x12布局96个位置"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 55.0
# 枪头尺寸
tip_diameter = 10.0
tip_spacing_x = 9.0 # X方向间距
tip_spacing_y = 9.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (12 - 1) * tip_spacing_x - tip_diameter) / 2
start_y = (carrier_size_y - (8 - 1) * tip_spacing_y - tip_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=12,
num_items_y=8,
dx=start_x,
dy=start_y,
dz=5.0,
item_dx=tip_spacing_x,
item_dy=tip_spacing_y,
size_x=tip_diameter,
size_y=tip_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_qiang_tou_he",
)
carrier.num_items_x = 12
carrier.num_items_y = 8
carrier.num_items_z = 1
# 创建96个枪头
for i in range(96):
row = chr(65 + i // 12) # A-H
col = (i % 12) + 1 # 1-12
carrier[i] = YB_qiang_tou(f"{name}_tip_{row}{col}")
return carrier

View File

@@ -1,163 +0,0 @@
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
# 工厂函数
"""加样头(大)"""
def YB_jia_yang_tou_da(
name: str,
diameter: float = 20.0,
height: float = 100.0,
max_volume: float = 30000.0, # 30mL
barcode: str = None,
) -> Bottle:
"""创建粉末瓶"""
return Bottle(
name=name,
diameter=diameter,# 未知
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_jia_yang_tou_da",
)
"""液1x1"""
def YB_ye_Bottle(
name: str,
diameter: float = 40.0,
height: float = 70.0,
max_volume: float = 50000.0, # 50mL
barcode: str = None,
) -> Bottle:
"""创建液体瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="Liquid_Bottle",
)
"""100ml液体"""
def YB_ye_100ml_Bottle(
name: str,
diameter: float = 50.0,
height: float = 90.0,
max_volume: float = 100000.0, # 100mL
barcode: str = None,
) -> Bottle:
"""创建100ml液体瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_100ml_yeti",
)
"""高粘液"""
def YB_gao_nian_ye_Bottle(
name: str,
diameter: float = 40.0,
height: float = 70.0,
max_volume: float = 50000.0, # 50mL
barcode: str = None,
) -> Bottle:
"""创建高粘液瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="High_Viscosity_Liquid",
)
"""5ml分液瓶"""
def YB_5ml_fenyeping(
name: str,
diameter: float = 20.0,
height: float = 50.0,
max_volume: float = 5000.0, # 5mL
barcode: str = None,
) -> Bottle:
"""创建5ml分液瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_5ml_fenyeping",
)
"""20ml分液瓶"""
def YB_20ml_fenyeping(
name: str,
diameter: float = 30.0,
height: float = 65.0,
max_volume: float = 20000.0, # 20mL
barcode: str = None,
) -> Bottle:
"""创建20ml分液瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_20ml_fenyeping",
)
"""配液瓶(小)"""
def YB_pei_ye_xiao_Bottle(
name: str,
diameter: float = 35.0,
height: float = 60.0,
max_volume: float = 30000.0, # 30mL
barcode: str = None,
) -> Bottle:
"""创建配液瓶(小)"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_pei_ye_xiao_Bottle",
)
"""配液瓶(大)"""
def YB_pei_ye_da_Bottle(
name: str,
diameter: float = 55.0,
height: float = 100.0,
max_volume: float = 150000.0, # 150mL
barcode: str = None,
) -> Bottle:
"""创建配液瓶(大)"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_pei_ye_da_Bottle",
)
"""枪头"""
def YB_qiang_tou(
name: str,
diameter: float = 10.0,
height: float = 50.0,
max_volume: float = 1000.0, # 1mL
barcode: str = None,
) -> Bottle:
"""创建枪头"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="YB_qiang_tou",
)

View File

@@ -1,209 +0,0 @@
from unilabos.resources.warehouse import WareHouse, YB_warehouse_factory
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return YB_warehouse_factory(
name=name,
num_items_x=1,
num_items_y=4,
num_items_z=4,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_1x4x2(name: str) -> WareHouse:
"""创建BioYond 4x1x2仓库"""
return YB_warehouse_factory(
name=name,
num_items_x=1,
num_items_y=4,
num_items_z=2,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
removed_positions=None
)
# 定义benyond的堆栈
def bioyond_warehouse_1x2x2(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
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",
)
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",
)
def bioyond_warehouse_10x1x1(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return YB_warehouse_factory(
name=name,
num_items_x=10,
num_items_y=1,
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="warehouse",
)
def bioyond_warehouse_1x3x3(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return YB_warehouse_factory(
name=name,
num_items_x=1,
num_items_y=3,
num_items_z=3,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_2x1x3(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return YB_warehouse_factory(
name=name,
num_items_x=2,
num_items_y=1,
num_items_z=3,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_3x3x1(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return YB_warehouse_factory(
name=name,
num_items_x=3,
num_items_y=3,
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="warehouse",
)
def bioyond_warehouse_5x1x1(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return YB_warehouse_factory(
name=name,
num_items_x=5,
num_items_y=1,
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="warehouse",
)
def bioyond_warehouse_3x3x1_2(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return YB_warehouse_factory(
name=name,
num_items_x=3,
num_items_y=3,
num_items_z=1,
dx=12.0,
dy=12.0,
dz=12.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse:
"""创建BioYond开关盖加液模块台面"""
return YB_warehouse_factory(
name=name,
num_items_x=2,
num_items_y=5,
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="warehouse",
removed_positions=None
)
def bioyond_warehouse_3x5x1(name: str) -> WareHouse:
"""创建BioYond 3x5x1仓库手动堆栈"""
return YB_warehouse_factory(
name=name,
num_items_x=3,
num_items_y=5,
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="warehouse",
)
def bioyond_warehouse_20x1x1(name: str) -> WareHouse:
"""创建BioYond 20x1x1仓库粉末加样头堆栈"""
return YB_warehouse_factory(
name=name,
num_items_x=20,
num_items_y=1,
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="warehouse",
)

View File

@@ -0,0 +1,276 @@
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.bottles import (
BIOYOND_PolymerStation_Solid_Stock,
BIOYOND_PolymerStation_Solid_Vial,
BIOYOND_PolymerStation_Liquid_Vial,
BIOYOND_PolymerStation_Solution_Beaker,
BIOYOND_PolymerStation_Reagent_Bottle
)
# 命名约定:试剂瓶-Bottle烧杯-Beaker烧瓶-Flask小瓶-Vial
def BIOYOND_Electrolyte_6VialCarrier(name: str) -> BottleCarrier:
"""6瓶载架 - 2x3布局"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 50.0
# 瓶位尺寸
bottle_diameter = 30.0
bottle_spacing_x = 42.0 # X方向间距
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=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="BIOYOND_Electrolyte_6VialCarrier",
)
carrier.num_items_x = 3
carrier.num_items_y = 2
carrier.num_items_z = 1
for i in range(6):
carrier[i] = BIOYOND_PolymerStation_Solid_Vial(f"{name}_vial_{i+1}")
return carrier
def BIOYOND_Electrolyte_1BottleCarrier(name: str) -> BottleCarrier:
"""1瓶载架 - 单个中央位置"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 100.0
# 烧杯尺寸
beaker_diameter = 80.0
# 计算中央位置
center_x = (carrier_size_x - beaker_diameter) / 2
center_y = (carrier_size_y - beaker_diameter) / 2
center_z = 5.0
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(center_x, center_y, center_z)],
resource_size_x=beaker_diameter,
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="BIOYOND_Electrolyte_1BottleCarrier",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = BIOYOND_PolymerStation_Solution_Beaker(f"{name}_beaker_1")
return carrier
def BIOYOND_PolymerStation_6StockCarrier(name: str) -> BottleCarrier:
"""6瓶载架 - 2x3布局"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 50.0
# 瓶位尺寸
bottle_diameter = 20.0
bottle_spacing_x = 42.0 # X方向间距
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=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="BIOYOND_PolymerStation_6VialCarrier",
)
carrier.num_items_x = 3
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序
for i in range(6):
carrier[i] = BIOYOND_PolymerStation_Solid_Stock(f"{name}_vial_{ordering[i]}")
return carrier
def BIOYOND_PolymerStation_6VialCarrier(name: str) -> BottleCarrier:
"""6瓶载架 - 2x3布局"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 50.0
# 瓶位尺寸
bottle_diameter = 30.0
bottle_spacing_x = 42.0 # X方向间距
bottle_spacing_y = 35.0 # Y方向间距
# 计算起始位置 (居中排列)
start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2
start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2
sites = create_ordered_items_2d(
klass=ResourceHolder,
num_items_x=3,
num_items_y=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="BIOYOND_PolymerStation_6VialCarrier",
)
carrier.num_items_x = 3
carrier.num_items_y = 2
carrier.num_items_z = 1
ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序
for i in range(3):
carrier[i] = BIOYOND_PolymerStation_Solid_Vial(f"{name}_solidvial_{ordering[i]}")
for i in range(3, 6):
carrier[i] = BIOYOND_PolymerStation_Liquid_Vial(f"{name}_liquidvial_{ordering[i]}")
return carrier
def BIOYOND_PolymerStation_1BottleCarrier(name: str) -> BottleCarrier:
"""1瓶载架 - 单个中央位置"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 20.0
# 烧杯尺寸
beaker_diameter = 60.0
# 计算中央位置
center_x = (carrier_size_x - beaker_diameter) / 2
center_y = (carrier_size_y - beaker_diameter) / 2
center_z = 5.0
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(center_x, center_y, center_z)],
resource_size_x=beaker_diameter,
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="BIOYOND_PolymerStation_1BottleCarrier",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = BIOYOND_PolymerStation_Reagent_Bottle(f"{name}_flask_1")
return carrier
def BIOYOND_PolymerStation_1FlaskCarrier(name: str) -> BottleCarrier:
"""1瓶载架 - 单个中央位置"""
# 载架尺寸 (mm)
carrier_size_x = 127.8
carrier_size_y = 85.5
carrier_size_z = 20.0
# 烧杯尺寸
beaker_diameter = 70.0
# 计算中央位置
center_x = (carrier_size_x - beaker_diameter) / 2
center_y = (carrier_size_y - beaker_diameter) / 2
center_z = 5.0
carrier = BottleCarrier(
name=name,
size_x=carrier_size_x,
size_y=carrier_size_y,
size_z=carrier_size_z,
sites=create_homogeneous_resources(
klass=ResourceHolder,
locations=[Coordinate(center_x, center_y, center_z)],
resource_size_x=beaker_diameter,
resource_size_y=beaker_diameter,
name_prefix=name,
),
model="BIOYOND_PolymerStation_1FlaskCarrier",
)
carrier.num_items_x = 1
carrier.num_items_y = 1
carrier.num_items_z = 1
carrier[0] = BIOYOND_PolymerStation_Reagent_Bottle(f"{name}_bottle_1")
return carrier

View File

@@ -0,0 +1,92 @@
from unilabos.resources.itemized_carrier import Bottle, BottleCarrier
# 工厂函数
def BIOYOND_PolymerStation_Solid_Stock(
name: str,
diameter: float = 20.0,
height: float = 100.0,
max_volume: float = 30000.0, # 30mL
barcode: str = None,
) -> Bottle:
"""创建粉末瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="BIOYOND_PolymerStation_Solid_Stock",
)
def BIOYOND_PolymerStation_Solid_Vial(
name: str,
diameter: float = 25.0,
height: float = 60.0,
max_volume: float = 30000.0, # 30mL
barcode: str = None,
) -> Bottle:
"""创建粉末瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="BIOYOND_PolymerStation_Solid_Vial",
)
def BIOYOND_PolymerStation_Liquid_Vial(
name: str,
diameter: float = 25.0,
height: float = 60.0,
max_volume: float = 30000.0, # 30mL
barcode: str = None,
) -> Bottle:
"""创建滴定液瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="BIOYOND_PolymerStation_Liquid_Vial",
)
def BIOYOND_PolymerStation_Solution_Beaker(
name: str,
diameter: float = 60.0,
height: float = 70.0,
max_volume: float = 200000.0, # 200mL
barcode: str = None,
) -> Bottle:
"""创建溶液烧杯"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="BIOYOND_PolymerStation_Solution_Beaker",
)
def BIOYOND_PolymerStation_Reagent_Bottle(
name: str,
diameter: float = 70.0,
height: float = 120.0,
max_volume: float = 500000.0, # 500mL
barcode: str = None,
) -> Bottle:
"""创建试剂瓶"""
return Bottle(
name=name,
diameter=diameter,
height=height,
max_volume=max_volume,
barcode=barcode,
model="BIOYOND_PolymerStation_Reagent_Bottle",
)

View File

@@ -1,8 +1,6 @@
from os import name
from pickle import TRUE
from pylabrobot.resources import Deck, Coordinate, Rotation from pylabrobot.resources import Deck, Coordinate, Rotation
from unilabos.resources.bioyond.YB_warehouses import bioyond_warehouse_1x4x4, bioyond_warehouse_1x4x2, bioyond_warehouse_liquid_and_lid_handling, bioyond_warehouse_1x2x2, bioyond_warehouse_1x3x3, bioyond_warehouse_10x1x1, bioyond_warehouse_3x3x1, bioyond_warehouse_3x3x1_2, bioyond_warehouse_5x1x1, bioyond_warehouse_20x1x1, bioyond_warehouse_2x2x1, bioyond_warehouse_3x5x1 from unilabos.resources.bioyond.warehouses import bioyond_warehouse_1x4x4, bioyond_warehouse_1x4x2, bioyond_warehouse_liquid_and_lid_handling
class BIOYOND_PolymerReactionStation_Deck(Deck): class BIOYOND_PolymerReactionStation_Deck(Deck):
@@ -36,6 +34,7 @@ class BIOYOND_PolymerReactionStation_Deck(Deck):
for warehouse_name, warehouse in self.warehouses.items(): for warehouse_name, warehouse in self.warehouses.items():
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name]) self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
class BIOYOND_PolymerPreparationStation_Deck(Deck): class BIOYOND_PolymerPreparationStation_Deck(Deck):
def __init__( def __init__(
self, self,
@@ -67,56 +66,3 @@ class BIOYOND_PolymerPreparationStation_Deck(Deck):
for warehouse_name, warehouse in self.warehouses.items(): for warehouse_name, warehouse in self.warehouses.items():
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name]) self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
class BIOYOND_YB_Deck(Deck):
def __init__(
self,
name: str = "YB_Deck",
size_x: float = 4150,
size_y: float = 1400.0,
size_z: float = 2670.0,
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()
def setup(self) -> None:
# 添加仓库
self.warehouses = {
"自动堆栈-左": bioyond_warehouse_2x2x1("自动堆栈-左"),
"自动堆栈-右": bioyond_warehouse_2x2x1("自动堆栈-右"),
"手动堆栈-左": bioyond_warehouse_3x5x1("手动堆栈-左"),
"手动堆栈-右": bioyond_warehouse_3x5x1("手动堆栈-右"),
"粉末加样头堆栈-左": bioyond_warehouse_10x1x1("粉末加样头堆栈-左"),
"粉末加样头堆栈-右": bioyond_warehouse_10x1x1("粉末加样头堆栈-右"),
"配液站内试剂仓库": bioyond_warehouse_3x3x1("配液站内试剂仓库"),
"试剂替换仓库-左": bioyond_warehouse_5x1x1("试剂替换仓库-左"),
"试剂替换仓库-右": bioyond_warehouse_5x1x1("试剂替换仓库-右"),
}
# warehouse 的位置
self.warehouse_locations = {
"自动堆栈-左": Coordinate(-300.0, 158.0, 0.0),
"自动堆栈-右": Coordinate(4160.0, 158.0, 0.0),
"手动堆栈-左": Coordinate(-400.0, 877.0, 0.0),
"手动堆栈-右": Coordinate(4160.0, 877.0, 0.0),
"粉末加样头堆栈-左": Coordinate(415.0, 1301.0, 0.0),
"粉末加样头堆栈-右": Coordinate(2200.0, 1304.0, 0.0),
"配液站内试剂仓库": Coordinate(2162.0, 337.0, 0.0),
"试剂替换仓库-左": Coordinate(1173.0, 702.0, 0.0),
"试剂替换仓库-右": Coordinate(2721.0, 739.0, 0.0),
}
for warehouse_name, warehouse in self.warehouses.items():
self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name])
# def YB_Deck(name: str) -> Deck:
# # by=BIOYOND_YB_Deck(name=name)
# # by.setup()
# return None

View File

@@ -0,0 +1,54 @@
from unilabos.resources.warehouse import WareHouse, warehouse_factory
def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库"""
return warehouse_factory(
name=name,
num_items_x=1,
num_items_y=4,
num_items_z=4,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
)
def bioyond_warehouse_1x4x2(name: str) -> WareHouse:
"""创建BioYond 4x1x2仓库"""
return warehouse_factory(
name=name,
num_items_x=1,
num_items_y=4,
num_items_z=2,
dx=10.0,
dy=10.0,
dz=10.0,
item_dx=137.0,
item_dy=96.0,
item_dz=120.0,
category="warehouse",
removed_positions=None
)
def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse:
"""创建BioYond开关盖加液模块台面"""
return warehouse_factory(
name=name,
num_items_x=2,
num_items_y=5,
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="warehouse",
removed_positions=None
)

View File

@@ -1,84 +1,67 @@
import json import json
from typing import Dict, Any
from pylabrobot.resources import Container
from unilabos_msgs.msg import Resource from unilabos_msgs.msg import Resource
from unilabos.ros.msgs.message_converter import convert_from_ros_msg from unilabos.ros.msgs.message_converter import convert_from_ros_msg
class RegularContainer(Container): class RegularContainer(object):
def __init__(self, *args, **kwargs): # 第一个参数必须是id传入
if "size_x" not in kwargs: # noinspection PyShadowingBuiltins
kwargs["size_x"] = 0 def __init__(self, id: str):
if "size_y" not in kwargs: self.id = id
kwargs["size_y"] = 0 self.ulr_resource = Resource()
if "size_z" not in kwargs: self._data = None
kwargs["size_z"] = 0
self.kwargs = kwargs
self.state = {}
super().__init__(*args, **kwargs)
def load_state(self, state: Dict[str, Any]): @property
self.state = state def ulr_resource_data(self):
# if self._data is None:
# class RegularContainer(object): self._data = json.loads(self.ulr_resource.data) if self.ulr_resource.data else {}
# # 第一个参数必须是id传入 return self._data
# # noinspection PyShadowingBuiltins
# def __init__(self, id: str): @ulr_resource_data.setter
# self.id = id def ulr_resource_data(self, value: dict):
# self.ulr_resource = Resource() self._data = value
# self._data = None self.ulr_resource.data = json.dumps(self._data)
#
# @property @property
# def ulr_resource_data(self): def liquid_type(self):
# if self._data is None: return self.ulr_resource_data.get("liquid_type", None)
# self._data = json.loads(self.ulr_resource.data) if self.ulr_resource.data else {}
# return self._data @liquid_type.setter
# def liquid_type(self, value: str):
# @ulr_resource_data.setter if value is not None:
# def ulr_resource_data(self, value: dict): self.ulr_resource_data["liquid_type"] = value
# self._data = value else:
# self.ulr_resource.data = json.dumps(self._data) self.ulr_resource_data.pop("liquid_type", None)
#
# @property @property
# def liquid_type(self): def liquid_volume(self):
# return self.ulr_resource_data.get("liquid_type", None) return self.ulr_resource_data.get("liquid_volume", None)
#
# @liquid_type.setter @liquid_volume.setter
# def liquid_type(self, value: str): def liquid_volume(self, value: float):
# if value is not None: if value is not None:
# self.ulr_resource_data["liquid_type"] = value self.ulr_resource_data["liquid_volume"] = value
# else: else:
# self.ulr_resource_data.pop("liquid_type", None) self.ulr_resource_data.pop("liquid_volume", None)
#
# @property def get_ulr_resource(self) -> Resource:
# def liquid_volume(self): """
# return self.ulr_resource_data.get("liquid_volume", None) 获取UlrResource对象
# :return: UlrResource对象
# @liquid_volume.setter """
# def liquid_volume(self, value: float): self.ulr_resource_data = self.ulr_resource_data # 确保数据被更新
# if value is not None: return self.ulr_resource
# self.ulr_resource_data["liquid_volume"] = value
# else: def get_ulr_resource_as_dict(self) -> Resource:
# self.ulr_resource_data.pop("liquid_volume", None) """
# 获取UlrResource对象
# def get_ulr_resource(self) -> Resource: :return: UlrResource对象
# """ """
# 获取UlrResource对象 to_dict = convert_from_ros_msg(self.get_ulr_resource())
# :return: UlrResource对象 to_dict["type"] = "container"
# """ return to_dict
# self.ulr_resource_data = self.ulr_resource_data # 确保数据被更新
# return self.ulr_resource def __str__(self):
# return f"{self.id}"
# def get_ulr_resource_as_dict(self) -> Resource:
# """
# 获取UlrResource对象
# :return: UlrResource对象
# """
# to_dict = convert_from_ros_msg(self.get_ulr_resource())
# to_dict["type"] = "container"
# return to_dict
#
# def __str__(self):
# return f"{self.id}"

View File

@@ -1,23 +1,18 @@
import importlib import importlib
import inspect import inspect
import json import json
import os.path
import traceback import traceback
from typing import Union, Any, Dict, List, Tuple from typing import Union, Any, Dict, List
import uuid
import networkx as nx import networkx as nx
from pylabrobot.resources import ResourceHolder from pylabrobot.resources import ResourceHolder
from unilabos_msgs.msg import Resource from unilabos_msgs.msg import Resource
from unilabos.config.config import BasicConfig
from unilabos.resources.container import RegularContainer from unilabos.resources.container import RegularContainer
from unilabos.resources.itemized_carrier import ItemizedCarrier
from unilabos.ros.msgs.message_converter import convert_to_ros_msg from unilabos.ros.msgs.message_converter import convert_to_ros_msg
from unilabos.ros.nodes.resource_tracker import ( from unilabos.ros.nodes.resource_tracker import (
ResourceDictInstance, ResourceDictInstance,
ResourceTreeSet, ResourceTreeSet,
) )
from unilabos.utils import logger
from unilabos.utils.banner_print import print_status from unilabos.utils.banner_print import print_status
try: try:
@@ -49,33 +44,6 @@ def canonicalize_nodes_data(
if node.get("label") is not None: if node.get("label") is not None:
node_id = node.pop("label") node_id = node.pop("label")
node["id"] = node["name"] = node_id node["id"] = node["name"] = node_id
if not isinstance(node.get("config"), dict):
node["config"] = {}
if not node.get("type"):
node["type"] = "device"
print_status(f"Warning: Node {node.get('id', 'unknown')} missing 'type', defaulting to 'device'", "warning")
if node.get("name", None) is None:
node["name"] = node.get("id")
print_status(f"Warning: Node {node.get('id', 'unknown')} missing 'name', defaulting to {node['name']}", "warning")
if not isinstance(node.get("position"), dict):
node["position"] = {"position": {}}
x = node.pop("x", None)
if x is not None:
node["position"]["position"]["x"] = x
y = node.pop("y", None)
if y is not None:
node["position"]["position"]["y"] = y
z = node.pop("z", None)
if z is not None:
node["position"]["position"]["z"] = z
if "sample_id" in node:
sample_id = node.pop("sample_id")
if sample_id:
logger.error(f"{node}的sample_id参数已弃用sample_id: {sample_id}")
for k in list(node.keys()):
if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data", "children"]:
v = node.pop(k)
node["config"][k] = v
# 第二步处理parent_relation # 第二步处理parent_relation
id2idx = {node["id"]: idx for idx, node in enumerate(nodes)} id2idx = {node["id"]: idx for idx, node in enumerate(nodes)}
@@ -333,10 +301,6 @@ def read_graphml(graphml_file: str) -> tuple[nx.Graph, ResourceTreeSet, List[Dic
"nodes": [node.res_content.model_dump(by_alias=True) for node in resource_tree_set.all_nodes], "nodes": [node.res_content.model_dump(by_alias=True) for node in resource_tree_set.all_nodes],
"links": standardized_links, "links": standardized_links,
} }
dump_json_path = os.path.join(BasicConfig.working_dir, os.path.basename(graphml_file).rsplit(".")[0] + ".json")
with open(dump_json_path, "w", encoding="utf-8") as f:
f.write(json.dumps(graph_data, indent=4, ensure_ascii=False))
print_status(f"GraphML converted to JSON and saved to {dump_json_path}", "info")
physical_setup_graph = nx.node_link_graph(graph_data, link="links", multigraph=False) physical_setup_graph = nx.node_link_graph(graph_data, link="links", multigraph=False)
handle_communications(physical_setup_graph) handle_communications(physical_setup_graph)
@@ -535,7 +499,6 @@ def resource_ulab_to_plr(resource: dict, plr_model=False) -> "ResourcePLR":
def resource_ulab_to_plr_inner(resource: dict): def resource_ulab_to_plr_inner(resource: dict):
all_states[resource["name"]] = resource["data"] all_states[resource["name"]] = resource["data"]
extra = resource.pop("extra", {})
d = { d = {
"name": resource["name"], "name": resource["name"],
"type": resource["type"], "type": resource["type"],
@@ -576,16 +539,16 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w
replace_info = { replace_info = {
"plate": "plate", "plate": "plate",
"well": "well", "well": "well",
"tip_spot": "tip_spot", "tip_spot": "container",
"trash": "trash", "trash": "container",
"deck": "deck", "deck": "deck",
"tip_rack": "tip_rack", "tip_rack": "container",
} }
if source in replace_info: if source in replace_info:
return replace_info[source] return replace_info[source]
else: else:
print("转换pylabrobot的时候出现未知类型", source) print("转换pylabrobot的时候出现未知类型", source)
return source return "container"
def resource_plr_to_ulab_inner(d: dict, all_states: dict, child=True) -> dict: def resource_plr_to_ulab_inner(d: dict, all_states: dict, child=True) -> dict:
r = { r = {
@@ -613,55 +576,51 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w
return r return r
def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[str, Tuple[str, str]] = {}, deck: Any = None) -> list[dict]: def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict = {}, deck: Any = None) -> list[dict]:
""" """
将 bioyond 物料格式转换为 ulab 物料格式 将 bioyond 物料格式转换为 ulab 物料格式
Args: Args:
bioyond_materials: bioyond 系统的物料查询结果列表 bioyond_materials: bioyond 系统的物料查询结果列表
type_mapping: 物料类型映射字典,格式 {bioyond_type: [plr_class_name, class_uuid]} type_mapping: 物料类型映射字典,格式 {bioyond_type: plr_class_name}
location_id_mapping: 库位 ID 到名称的映射字典,格式 {location_id: location_name} location_id_mapping: 库位 ID 到名称的映射字典,格式 {location_id: location_name}
Returns: Returns:
pylabrobot 格式的物料列表 pylabrobot 格式的物料列表
""" """
print("1:bioyond_materials:",bioyond_materials)
# print("2:type_mapping:",type_mapping)
plr_materials = [] plr_materials = []
for material in bioyond_materials: for material in bioyond_materials:
className = ( className = (
type_mapping.get(material.get("typeName"), ("RegularContainer", ""))[0] if type_mapping else "RegularContainer" type_mapping.get(material.get("typeName"), "RegularContainer") if type_mapping else "RegularContainer"
) )
plr_material: ResourcePLR = initialize_resource( plr_material: ResourcePLR = initialize_resource(
{"name": material["name"], "class": className}, resource_type=ResourcePLR {"name": material["name"], "class": className}, resource_type=ResourcePLR
) )
print("plr_material:",plr_material)
print("code:",material.get("code", ""))
plr_material.code = material.get("code", "") and material.get("barCode", "") or "" plr_material.code = material.get("code", "") and material.get("barCode", "") or ""
plr_material.unilabos_uuid = str(uuid.uuid4())
# 处理子物料detail # 处理子物料detail
if material.get("detail") and len(material["detail"]) > 0: if material.get("detail") and len(material["detail"]) > 0:
for bottle in reversed(plr_material.children):
plr_material.unassign_child_resource(bottle)
child_ids = [] child_ids = []
for detail in material["detail"]: for detail in material["detail"]:
number = ( number = (
(detail.get("z", 0) - 1) * plr_material.num_items_x * plr_material.num_items_y (detail.get("z", 0) - 1) * plr_material.num_items_x * plr_material.num_items_y
+ (detail.get("y", 0) - 1) * plr_material.num_items_y + (detail.get("x", 0) - 1) * plr_material.num_items_x
+ (detail.get("x", 0) - 1) + (detail.get("y", 0) - 1)
) )
typeName = detail.get("typeName", detail.get("name", "")) bottle = plr_material[number]
if typeName in type_mapping: if detail["name"] in type_mapping:
bottle = plr_material[number] = initialize_resource( # plr_material.unassign_child_resource(bottle)
{"name": f'{detail["name"]}_{number}', "class": type_mapping[typeName][0]}, resource_type=ResourcePLR plr_material.sites[number] = None
plr_material[number] = initialize_resource(
{"name": f'{detail["name"]}_{number}', "class": type_mapping[detail["name"]]}, resource_type=ResourcePLR
) )
else:
bottle.tracker.liquids = [ bottle.tracker.liquids = [
(detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0) (detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0)
] ]
bottle.code = detail.get("code", "") bottle.code = detail.get("code", "")
else: else:
bottle = plr_material[0] if plr_material.capacity > 0 else plr_material bottle = plr_material[0] if plr_material.capacity > 0 else plr_material
bottle.tracker.liquids = [ bottle.tracker.liquids = [
@@ -686,59 +645,32 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[st
return plr_materials return plr_materials
def resource_plr_to_bioyond(plr_resources: list[ResourcePLR], type_mapping: dict = {}, warehouse_mapping: dict = {}) -> list[dict]: def resource_plr_to_bioyond(plr_materials: list[ResourcePLR], type_mapping: dict = {}, warehouse_mapping: dict = {}) -> list[dict]:
bioyond_materials = [] bioyond_materials = []
for resource in plr_resources: for plr_material in plr_materials:
if hasattr(resource, "capacity") and resource.capacity > 1: material = {
material = { "name": plr_material.name,
"typeId": type_mapping.get(resource.model)[1], "typeName": plr_material.__class__.__name__,
"name": resource.name, "code": plr_material.code,
"unit": "", "quantity": 0,
"quantity": 1, "detail": [],
"details": [], "locations": [],
"Parameters": "{}" }
} if hasattr(plr_material, "capacity") and plr_material.capacity > 1:
for bottle in resource.children: for idx in range(plr_material.capacity):
if isinstance(resource, ItemizedCarrier): bottle = plr_material[idx]
site = resource.get_child_identifier(bottle) detail = {
else: "x": (idx // (plr_material.num_items_x * plr_material.num_items_y)) + 1,
site = {"x": bottle.location.x - 1, "y": bottle.location.y - 1} "y": ((idx % (plr_material.num_items_x * plr_material.num_items_y)) // plr_material.num_items_x) + 1,
detail_item = { "z": (idx % plr_material.num_items_x) + 1,
"typeId": type_mapping.get(bottle.model)[1],
"name": bottle.name,
"code": bottle.code if hasattr(bottle, "code") else "", "code": bottle.code if hasattr(bottle, "code") else "",
"quantity": sum(qty for _, qty in bottle.tracker.liquids) if hasattr(bottle, "tracker") else 0, "quantity": sum(qty for _, qty in bottle.tracker.liquids) if hasattr(bottle, "tracker") else 0,
"x": site["x"] + 1,
"y": site["y"] + 1,
"molecular": 1,
"Parameters": json.dumps({"molecular": 1})
} }
material["details"].append(detail_item) material["detail"].append(detail)
material["quantity"] = 1.0
else: else:
bottle = resource[0] if resource.capacity > 0 else resource bottle = plr_material[0] if plr_material.capacity > 0 else plr_material
material = { material["quantity"] = sum(qty for _, qty in bottle.tracker.liquids) if hasattr(plr_material, "tracker") else 0
"typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
"name": resource.get("name", ""),
"unit": "",
"quantity": sum(qty for _, qty in bottle.tracker.liquids) if hasattr(bottle, "tracker") else 0,
"Parameters": "{}"
}
if resource.parent is not None and isinstance(resource.parent, ItemizedCarrier):
site_in_parent = resource.parent.get_child_identifier(resource)
material["locations"] = [
{
"id": warehouse_mapping[resource.parent.name]["site_uuids"][site_in_parent["identifier"]],
"whid": warehouse_mapping[resource.parent.name]["uuid"],
"whName": resource.parent.name,
"x": site_in_parent["z"] + 1,
"y": site_in_parent["y"] + 1,
"z": 1,
"quantity": 0
}
],
print(f"material_data: {material}")
bioyond_materials.append(material) bioyond_materials.append(material)
return bioyond_materials return bioyond_materials
@@ -785,7 +717,6 @@ def initialize_resource(resource_config: dict, resource_type: Any = None) -> Uni
else: else:
r = resource_plr r = resource_plr
elif resource_class_config["type"] == "unilabos": elif resource_class_config["type"] == "unilabos":
raise ValueError(f"No more support for unilabos Resource class {resource_class_config}")
res_instance: RegularContainer = RESOURCE(id=resource_config["name"]) res_instance: RegularContainer = RESOURCE(id=resource_config["name"])
res_instance.ulr_resource = convert_to_ros_msg( res_instance.ulr_resource = convert_to_ros_msg(
Resource, {k: v for k, v in resource_config.items() if k != "class"} Resource, {k: v for k, v in resource_config.items() if k != "class"}

View File

@@ -32,7 +32,6 @@ class Bottle(Well):
barcode: Optional[str] = "", barcode: Optional[str] = "",
category: str = "container", category: str = "container",
model: Optional[str] = None, model: Optional[str] = None,
**kwargs,
): ):
super().__init__( super().__init__(
name=name, name=name,
@@ -74,11 +73,9 @@ class ItemizedCarrier(ResourcePLR):
num_items_x: int = 0, num_items_x: int = 0,
num_items_y: int = 0, num_items_y: int = 0,
num_items_z: int = 0, num_items_z: int = 0,
layout: str = "x-y",
sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None, sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None,
category: Optional[str] = "carrier", category: Optional[str] = "carrier",
model: Optional[str] = None, model: Optional[str] = None,
invisible_slots: Optional[str] = None,
): ):
super().__init__( super().__init__(
name=name, name=name,
@@ -90,9 +87,6 @@ class ItemizedCarrier(ResourcePLR):
) )
self.num_items = len(sites) self.num_items = len(sites)
self.num_items_x, self.num_items_y, self.num_items_z = num_items_x, num_items_y, num_items_z self.num_items_x, self.num_items_y, self.num_items_z = num_items_x, num_items_y, num_items_z
self.invisible_slots = [] if invisible_slots is None else invisible_slots
self.layout = "z-y" if self.num_items_z > 1 and self.num_items_x == 1 else "x-z" if self.num_items_z > 1 and self.num_items_y == 1 else "x-y"
if isinstance(sites, dict): if isinstance(sites, dict):
sites = sites or {} sites = sites or {}
self.sites: List[Optional[ResourcePLR]] = list(sites.values()) self.sites: List[Optional[ResourcePLR]] = list(sites.values())
@@ -155,7 +149,7 @@ class ItemizedCarrier(ResourcePLR):
def assign_resource_to_site(self, resource: ResourcePLR, spot: int): def assign_resource_to_site(self, resource: ResourcePLR, spot: int):
if self.sites[spot] is not None and not isinstance(self.sites[spot], ResourceHolder): if self.sites[spot] is not None and not isinstance(self.sites[spot], ResourceHolder):
raise ValueError(f"spot {spot} already has a resource, {resource}") raise ValueError(f"spot {spot} already has a resource, {resource}")
self.assign_child_resource(resource, location=self.child_locations.get(list(self._ordering.keys())[spot]), spot=spot) self.assign_child_resource(resource, location=self.child_locations.get(str(spot)), spot=spot)
def unassign_child_resource(self, resource: ResourcePLR): def unassign_child_resource(self, resource: ResourcePLR):
found = False found = False
@@ -166,92 +160,8 @@ class ItemizedCarrier(ResourcePLR):
break break
if not found: if not found:
raise ValueError(f"Resource {resource} is not assigned to this carrier") raise ValueError(f"Resource {resource} is not assigned to this carrier")
super().unassign_child_resource(resource) if hasattr(resource, "unassign"):
# if hasattr(resource, "unassign"): resource.unassign()
# resource.unassign()
def get_child_identifier(self, child: ResourcePLR):
"""Get the identifier information for a given child resource.
Args:
child: The Resource object to find the identifier for
Returns:
dict: A dictionary containing:
- identifier: The string identifier (e.g. "A1", "B2")
- idx: The integer index in the sites list
- x: The x index (column index, 0-based)
- y: The y index (row index, 0-based)
- z: The z index (layer index, 0-based)
Raises:
ValueError: If the child resource is not found in this carrier
"""
# Find the child resource in sites
for idx, resource in enumerate(self.sites):
if resource is child:
# Get the identifier from ordering keys
identifier = list(self._ordering.keys())[idx]
# Parse identifier to get x, y, z indices
x_idx, y_idx, z_idx = self._parse_identifier_to_indices(identifier, idx)
return {
"identifier": identifier,
"idx": idx,
"x": x_idx,
"y": y_idx,
"z": z_idx
}
# If not found, raise an error
raise ValueError(f"Resource {child} is not assigned to this carrier")
def _parse_identifier_to_indices(self, identifier: str, idx: int) -> Tuple[int, int, int]:
"""Parse identifier string to get x, y, z indices.
Args:
identifier: String identifier like "A1", "B2", etc.
idx: Linear index as fallback for calculation
Returns:
Tuple of (x_idx, y_idx, z_idx)
"""
# If we have explicit dimensions, calculate from idx
if self.num_items_x > 0 and self.num_items_y > 0:
# Calculate 3D indices from linear index
z_idx = idx // (self.num_items_x * self.num_items_y) if self.num_items_z > 0 else 0
remaining = idx % (self.num_items_x * self.num_items_y)
y_idx = remaining // self.num_items_x
x_idx = remaining % self.num_items_x
return x_idx, y_idx, z_idx
# Fallback: parse from Excel-style identifier
if isinstance(identifier, str) and len(identifier) >= 2:
# Extract row (letter) and column (number)
row_letters = ""
col_numbers = ""
for char in identifier:
if char.isalpha():
row_letters += char
elif char.isdigit():
col_numbers += char
if row_letters and col_numbers:
# Convert letter(s) to row index (A=0, B=1, etc.)
y_idx = 0
for char in row_letters:
y_idx = y_idx * 26 + (ord(char.upper()) - ord('A'))
# Convert number to column index (1-based to 0-based)
x_idx = int(col_numbers) - 1
z_idx = 0 # Default layer
return x_idx, y_idx, z_idx
# If all else fails, assume linear arrangement
return idx, 0, 0
def __getitem__( def __getitem__(
self, self,
@@ -409,10 +319,9 @@ class ItemizedCarrier(ResourcePLR):
"num_items_x": self.num_items_x, "num_items_x": self.num_items_x,
"num_items_y": self.num_items_y, "num_items_y": self.num_items_y,
"num_items_z": self.num_items_z, "num_items_z": self.num_items_z,
"layout": self.layout,
"sites": [{ "sites": [{
"label": str(identifier), "label": str(identifier),
"visible": False if identifier in self.invisible_slots else True, "visible": True if self[identifier] is not None else False,
"occupied_by": self[identifier].name "occupied_by": self[identifier].name
if isinstance(self[identifier], ResourcePLR) and not isinstance(self[identifier], ResourceHolder) else if isinstance(self[identifier], ResourcePLR) and not isinstance(self[identifier], ResourceHolder) else
self[identifier] if isinstance(self[identifier], str) else None, self[identifier] if isinstance(self[identifier], str) else None,
@@ -435,8 +344,6 @@ class BottleCarrier(ItemizedCarrier):
sites: Optional[Dict[Union[int, str], ResourceHolder]] = None, sites: Optional[Dict[Union[int, str], ResourceHolder]] = None,
category: str = "bottle_carrier", category: str = "bottle_carrier",
model: Optional[str] = None, model: Optional[str] = None,
invisible_slots: List[str] = None,
**kwargs,
): ):
super().__init__( super().__init__(
name=name, name=name,
@@ -446,5 +353,4 @@ class BottleCarrier(ItemizedCarrier):
sites=sites, sites=sites,
category=category, category=category,
model=model, model=model,
invisible_slots=invisible_slots,
) )

View File

@@ -5,13 +5,10 @@ from pylabrobot.resources.carrier import ResourceHolder, create_homogeneous_reso
from unilabos.resources.itemized_carrier import ItemizedCarrier, ResourcePLR from unilabos.resources.itemized_carrier import ItemizedCarrier, ResourcePLR
LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" def warehouse_factory(
def YB_warehouse_factory(
name: str, name: str,
num_items_x: int = 1, num_items_x: int = 4,
num_items_y: int = 4, num_items_y: int = 1,
num_items_z: int = 4, num_items_z: int = 4,
dx: float = 137.0, dx: float = 137.0,
dy: float = 96.0, dy: float = 96.0,
@@ -36,17 +33,13 @@ def YB_warehouse_factory(
locations.append(Coordinate(x, y, z)) locations.append(Coordinate(x, y, z))
if removed_positions: if removed_positions:
locations = [loc for i, loc in enumerate(locations) if i not in removed_positions] locations = [loc for i, loc in enumerate(locations) if i not in removed_positions]
_sites = create_homogeneous_resources( sites = create_homogeneous_resources(
klass=ResourceHolder, klass=ResourceHolder,
locations=locations, locations=locations,
resource_size_x=127.0, resource_size_x=127.0,
resource_size_y=86.0, resource_size_y=86.0,
name_prefix=name, name_prefix=name,
) )
len_x, len_y = (num_items_x, num_items_y) if num_items_z == 1 else (num_items_y, num_items_z) if num_items_x == 1 else (num_items_x, num_items_z)
keys = [f"{LETTERS[len_y-1-j]}{str(i+1).zfill(2)}" for j in range(len_y) for i in range(len_x)]
sites = {i: site for i, site in zip(keys, _sites.values())}
return WareHouse( return WareHouse(
name=name, name=name,
@@ -75,7 +68,6 @@ class WareHouse(ItemizedCarrier):
num_items_x: int, num_items_x: int,
num_items_y: int, num_items_y: int,
num_items_z: int, num_items_z: int,
layout: str = "x-y",
sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None, sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None,
category: str = "warehouse", category: str = "warehouse",
model: Optional[str] = None, model: Optional[str] = None,
@@ -91,7 +83,6 @@ class WareHouse(ItemizedCarrier):
num_items_x=num_items_x, num_items_x=num_items_x,
num_items_y=num_items_y, num_items_y=num_items_y,
num_items_z=num_items_z, num_items_z=num_items_z,
layout=layout,
sites=sites, sites=sites,
category=category, category=category,
model=model, model=model,

View File

@@ -26,7 +26,6 @@ def initialize_device_from_dict(device_id, device_config) -> Optional[ROS2Device
d = None d = None
original_device_config = copy.deepcopy(device_config) original_device_config = copy.deepcopy(device_config)
device_class_config = device_config["class"] device_class_config = device_config["class"]
uid = device_config["uuid"]
if isinstance(device_class_config, str): # 如果是字符串则直接去lab_registry中查找获取class if isinstance(device_class_config, str): # 如果是字符串则直接去lab_registry中查找获取class
if len(device_class_config) == 0: if len(device_class_config) == 0:
raise DeviceClassInvalid(f"Device [{device_id}] class cannot be an empty string. {device_config}") raise DeviceClassInvalid(f"Device [{device_id}] class cannot be an empty string. {device_config}")
@@ -51,7 +50,7 @@ def initialize_device_from_dict(device_id, device_config) -> Optional[ROS2Device
) )
try: try:
d = DEVICE( d = DEVICE(
device_id=device_id, device_uuid=uid, driver_is_ros=device_class_config["type"] == "ros2", driver_params=device_config.get("config", {}) device_id=device_id, driver_is_ros=device_class_config["type"] == "ros2", driver_params=device_config.get("config", {})
) )
except DeviceInitError as ex: except DeviceInitError as ex:
return d return d

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