Compare commits

..

252 Commits

Author SHA1 Message Date
ZiWei
9f7c3f02f9 fix(bioyond/warehouses): 修正仓库尺寸和物品排列参数
调整仓库的x轴和z轴物品数量以及物品尺寸参数,使其符合4x1x4的规格要求
2025-10-19 08:36:40 +08:00
Junhan Chang
19dd80dcdb fix warehouse serialize/deserialize 2025-10-19 08:18:18 +08:00
Junhan Chang
9d5ed627a2 fix bioyond converter 2025-10-19 05:21:41 +08:00
Junhan Chang
2d0ff87bc8 fix itemized_carrier.unassign_child_resource 2025-10-19 05:19:19 +08:00
Junhan Chang
d78475de9a allow not-loaded MSG in registry 2025-10-19 05:18:15 +08:00
Junhan Chang
88ae56806c add layout serializer & converter 2025-10-18 20:53:03 +08:00
Junhan Chang
95dd8beb81 warehouseuse A1-D4; add warehouse layout 2025-10-18 20:27:50 +08:00
ZiWei
4ab3fadbec fix(graphio): 修正bioyond到plr资源转换中的坐标计算错误 2025-10-18 19:25:23 +08:00
ZiWei
229888f834 Fix resource assignment and type mapping issues
Corrects resource assignment in ItemizedCarrier by using the correct spot key from _ordering. Updates graphio to use 'typeName' instead of 'name' for type mapping in resource_bioyond_to_plr. Renames DummyWorkstation to BioyondWorkstation in workstation_http_service for clarity.
2025-10-18 18:55:16 +08:00
ZiWei
b443b39ebf Merge branch 'dev' of https://github.com/dptech-corp/Uni-Lab-OS into dev 2025-10-18 16:49:22 +08:00
Junhan Chang
0434bbc15b add more enumeration in POSE 2025-10-18 16:46:34 +08:00
ZiWei
5791b81954 Merge branch 'dev' of https://github.com/dptech-corp/Uni-Lab-OS into dev 2025-10-18 16:23:32 +08:00
Junhan Chang
bd51c74fab fix converter in resource_tracker 2025-10-18 16:22:30 +08:00
ZiWei
ba81cbddf8 Merge branch 'dev' of https://github.com/dptech-corp/Uni-Lab-OS into dev 2025-10-18 10:51:04 +08:00
ZiWei
4e92a26057 fix(reaction_station): 清空工作流序列和参数避免重复执行 (#113)
在创建任务后清空工作流序列和参数,防止下次执行时累积重复
2025-10-17 13:41:50 +08:00
ZiWei
c2895bb197 fix(reaction_station): 清空工作流序列和参数避免重复累积 2025-10-17 13:13:54 +08:00
ZiWei
0423f4f452 Merge branch 'dev' of https://github.com/dptech-corp/Uni-Lab-OS into dev 2025-10-17 13:00:32 +08:00
Junhan Chang
41390fbef9 fix resource_get in action 2025-10-17 11:18:47 +08:00
ZiWei
98bdb4e7e4 Merge branch 'dev' of https://github.com/dptech-corp/Uni-Lab-OS into dev 2025-10-17 03:06:04 +08:00
Xuwznln
30037a077a correct return message 2025-10-17 03:03:08 +08:00
ZiWei
6972680099 Refactor Bioyond workstation and experiment workflow -fix (#111)
* refactor(bioyond_studio): 优化材料缓存加载和参数验证逻辑

改进材料缓存加载逻辑以支持多种材料类型和详细材料处理
更新工作流参数验证中的字段名从key/value改为Key/DisplayValue
移除未使用的merge_workflow_with_parameters方法
添加get_station_info方法获取工作站基础信息
清理实验文件中的注释代码和更新导入路径

* fix: 修复资源移除时的父资源检查问题

在BaseROS2DeviceNode中,移除资源前添加对父资源是否为None的检查,避免空指针异常
同时更新Bottle和BottleCarrier类以支持**kwargs参数
修正测试文件中Liquid_feeding_beaker的大小写拼写错误
2025-10-17 02:59:58 +08:00
ZiWei
9d2c93807d Merge branch 'dev' of https://github.com/dptech-corp/Uni-Lab-OS into dev 2025-10-17 02:40:33 +08:00
Xuwznln
e728007bc5 cancel upload_registry 2025-10-17 02:34:59 +08:00
ZiWei
9c5ecda7cc Refactor Bioyond workstation and experiment workflow (#110)
Refactored the Bioyond workstation classes to improve parameter handling and workflow management. Updated experiment.py to use BioyondReactionStation with deck and material mappings, and enhanced workflow step parameter mapping and execution logic. Adjusted JSON experiment configs, improved workflow sequence handling, and added UUID assignment to PLR materials. Removed unused station_config and material cache logic, and added detailed docstrings and debug output for workflow methods.
2025-10-17 02:32:13 +08:00
ZiWei
2d26c3fac6 fix: 修复资源移除时的父资源检查问题
在BaseROS2DeviceNode中,移除资源前添加对父资源是否为None的检查,避免空指针异常
同时更新Bottle和BottleCarrier类以支持**kwargs参数
修正测试文件中Liquid_feeding_beaker的大小写拼写错误
2025-10-17 02:23:58 +08:00
ZiWei
f5753afb7c refactor(bioyond_studio): 优化材料缓存加载和参数验证逻辑
改进材料缓存加载逻辑以支持多种材料类型和详细材料处理
更新工作流参数验证中的字段名从key/value改为Key/DisplayValue
移除未使用的merge_workflow_with_parameters方法
添加get_station_info方法获取工作站基础信息
清理实验文件中的注释代码和更新导入路径
2025-10-16 23:58:24 +08:00
Xuwznln
398b2dde3f Close #107
Update doc url.
2025-10-16 14:44:09 +08:00
Xuwznln
62c4135938 Update deploy-docs.yml 2025-10-16 14:28:55 +08:00
Xuwznln
027b4269c4 Update deploy-docs.yml 2025-10-16 14:23:22 +08:00
Xuwznln
3757bd9c58 fix state loading for regular container 2025-10-16 14:04:03 +08:00
Xuwznln
c75b7d5aae fix type conversion 2025-10-16 13:55:38 +08:00
Xuwznln
dfc635189c fix comprehensive_station.json 2025-10-16 13:52:07 +08:00
Xuwznln
d8f3ebac15 fix comprehensive_station.json 2025-10-16 13:46:59 +08:00
Xuwznln
4a1e703a3a support no size init 2025-10-16 13:35:59 +08:00
Xuwznln
55d22a7c29 Update regular container method 2025-10-16 13:33:28 +08:00
Xuwznln
03a4e4ecba fix to plr type error 2025-10-16 13:19:59 +08:00
Xuwznln
2316c34cb5 fix to plr type error 2025-10-16 13:12:21 +08:00
Xuwznln
a8887161d3 pack repo info 2025-10-16 13:06:13 +08:00
Xuwznln
25834f5ba0 provide error info when cant find plr type 2025-10-16 13:05:44 +08:00
Xuwznln
a1e9332b51 temp fix for resource get 2025-10-16 03:20:37 +08:00
Xuwznln
357fc038ef temp fix for resource get 2025-10-16 03:15:56 +08:00
Xuwznln
fd58ef07f3 Update boot example 2025-10-16 02:33:15 +08:00
Xuwznln
93dee2c1dc fix workstation node error 2025-10-16 01:59:48 +08:00
Xuwznln
70fbf19009 fix workstation node error 2025-10-16 01:58:15 +08:00
ZiWei
9149155232 Add logging configuration based on BasicConfig in main function 2025-10-14 21:02:15 +08:00
Xuwznln
1ca1792e3c mount parent uuid 2025-10-14 18:07:59 +08:00
Xuwznln
485e7e8dd2 Fix resource get.
Fix resource parent not found.
Mapping uuid for all resources.
2025-10-14 17:24:41 +08:00
ZiWei
4ddabdcb65 Refactor Bioyond workstation and experiment workflow (#105)
Refactored the Bioyond workstation classes to improve parameter handling and workflow management. Updated experiment.py to use BioyondReactionStation with deck and material mappings, and enhanced workflow step parameter mapping and execution logic. Adjusted JSON experiment configs, improved workflow sequence handling, and added UUID assignment to PLR materials. Removed unused station_config and material cache logic, and added detailed docstrings and debug output for workflow methods.
2025-10-14 02:46:31 +08:00
Xuwznln
a5b0325301 Tip more error log 2025-10-14 02:29:14 +08:00
Xuwznln
50b44938c7 Force confirm uuid 2025-10-14 02:22:39 +08:00
Xuwznln
df0d2235b0 Fix resource tree update 2025-10-14 01:55:29 +08:00
Xuwznln
4e434eeb97 Fix resource tree update 2025-10-14 01:53:04 +08:00
Xuwznln
ca027bf0eb Fix multiple resource error 2025-10-14 01:45:08 +08:00
Xuwznln
635a332b4e Fix workstation deck & children resource dupe 2025-10-14 00:21:37 +08:00
Xuwznln
edf7a117ca Fix workstation deck & children resource dupe 2025-10-14 00:21:16 +08:00
Xuwznln
70b2715996 Fix workstation resource not tracking 2025-10-14 00:05:41 +08:00
Xuwznln
7e8dfc2dc5 Fix children key error 2025-10-13 23:34:17 +08:00
Xuwznln
9b626489a8 Fix children key error 2025-10-13 21:20:42 +08:00
Xuwznln
03fe208743 Raise error when using unsupported type to create ResourceTreeSet 2025-10-13 15:20:30 +08:00
Xuwznln
e913e540a3 Fix ResourceTreeSet load error 2025-10-13 15:16:56 +08:00
Xuwznln
aed39b648d Fix workstation startup
Update registry
2025-10-13 15:01:46 +08:00
Xuwznln
8c8359fab3 Fix one-key installation build for windows 2025-10-13 15:01:46 +08:00
Xuwznln
5d20be0762 Fix conda pack on windows
(cherry picked from commit 2a8e8d014b)
2025-10-13 13:20:20 +08:00
Junhan Chang
09f745d300 modify default config 2025-10-13 10:49:15 +08:00
Junhan Chang
bbcbcde9a4 add plr_to_bioyond, and refactor bioyond stations 2025-10-13 09:41:43 +08:00
hh.(SII)
42b437cdea fix: rename schema field to resource_schema with serialization and validation aliases (#104)
Co-authored-by: ZiWei <131428629+ZiWei09@users.noreply.github.com>
2025-10-13 03:23:04 +08:00
Xuwznln
ffd0f2d26a Complete all one key installation 2025-10-13 03:21:16 +08:00
Xuwznln
32422c0b3d Install conda-pack before pack command 2025-10-13 03:09:44 +08:00
Xuwznln
c44e597dc0 Add conda-pack to base when building one-key installer 2025-10-13 03:01:48 +08:00
Xuwznln
4eef012a8e Fix param error when using mamba run 2025-10-13 02:50:33 +08:00
Xuwznln
ac69452f3c Try fix one-key build on linux 2025-10-13 02:35:06 +08:00
Xuwznln
57b30f627b Try fixx one-key build on linux 2025-10-13 02:24:03 +08:00
Xuwznln
2d2a4ca067 Try fix one-key build on linux
(cherry picked from commit eb1f3fbe1c)
2025-10-13 02:10:20 +08:00
Xuwznln
a2613aad4c fix startup env check.
add auto install during one-key installation
2025-10-13 01:35:28 +08:00
Xuwznln
54f75183ff clean files 2025-10-12 23:26:49 +08:00
Xuwznln
735be067dc fix ony-key script not exist 2025-10-12 23:10:06 +08:00
Xuwznln
0fe62d64f0 Update registry from pr 2025-10-12 23:04:25 +08:00
shiyubo0410
2d4ecec1e1 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
2025-10-12 22:56:39 +08:00
lixinyu1011
0f976a1874 电池装配工站二次开发教程(带目录)上传至dev (#94)
* 电池装配工站二次开发教程

* Update intro.md

* 物料教程

* 更新物料教程,json格式注释
2025-10-12 22:56:14 +08:00
ZiWei
b263a7e679 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__ 导出列表
- 确保所有主要类都可以正确导入

* 修复大小写文件夹名字
2025-10-12 22:54:38 +08:00
Xuwznln
7c7f1b31c5 Bump version to 0.10.7 2025-10-12 22:52:34 +08:00
Xuwznln
00e668e140 Fix one-key installation path error 2025-10-12 22:49:51 +08:00
Xuwznln
4989f65a0b Fix nested conda pack 2025-10-12 22:45:05 +08:00
Xuwznln
9fa3688196 Update registry. Update uuid loop figure method. Update install docs. 2025-10-12 22:38:04 +08:00
Xuwznln
40fb1ea49c Merge branch 'main' into dev
# Conflicts:
#	.conda/recipe.yaml
#	.github/workflows/conda-pack-build.yml
#	recipes/msgs/recipe.yaml
#	recipes/unilabos/recipe.yaml
#	scripts/verify_installation.py
#	setup.py
#	unilabos/app/main.py
#	unilabos/app/mq.py
#	unilabos/app/register.py
#	unilabos/compile/heatchill_protocol.py
#	unilabos/compile/separate_protocol.py
#	unilabos/config/config.py
#	unilabos/devices/pump_and_valve/runze_backbone.py
#	unilabos/devices/pump_and_valve/runze_multiple_backbone.py
#	unilabos/registry/devices/characterization_chromatic.yaml
#	unilabos/registry/devices/liquid_handler.yaml
#	unilabos/registry/devices/pump_and_valve.yaml
#	unilabos/registry/devices/robot_arm.yaml
#	unilabos/registry/devices/robot_linear_motion.yaml
#	unilabos/registry/devices/work_station.yaml
#	unilabos/registry/registry.py
#	unilabos/registry/resources/organic/workstation.yaml
#	unilabos/resources/plr_additional_res_reg.py
#	unilabos/ros/nodes/base_device_node.py
#	unilabos/ros/nodes/presets/host_node.py
#	unilabos/ros/nodes/presets/workstation.py
#	unilabos/ros/nodes/resource_tracker.py
#	unilabos/utils/environment_check.py
#	unilabos_msgs/package.xml
2025-10-12 22:13:49 +08:00
Xuwznln
18b0bb397e Update recipe.yaml 2025-10-12 22:12:46 +08:00
Xuwznln
65abc5dbf7 Fix environment_check.py 2025-10-12 21:55:34 +08:00
Xuwznln
2455ca15ba Fix unilabos msgs search error 2025-10-12 21:39:06 +08:00
Xuwznln
05a3ff607a Try fix 'charmap' codec can't encode characters in position 16-23: character maps to <undefined> 2025-10-12 21:23:29 +08:00
Xuwznln
ec882df36d Fix FileNotFoundError 2025-10-12 21:00:18 +08:00
Xuwznln
43b992e3eb Update conda-pack-build.yml 2025-10-12 20:47:06 +08:00
Xuwznln
6422fa5a9a Update conda-pack-build.yml (with mamba) 2025-10-12 20:34:49 +08:00
Xuwznln
434b9e98e0 Update conda-pack-build.yml 2025-10-12 20:28:38 +08:00
Xuwznln
040073f430 Add version in __init__.py
Update conda-pack-build.yml
Add create_zip_archive.py
2025-10-12 20:28:04 +08:00
Xuwznln
3d95c9896a update conda-pack-build.yml 2025-10-12 19:41:34 +08:00
Xuwznln
9aa97ed01e update conda-pack-build.yml 2025-10-12 19:38:11 +08:00
Xuwznln
0b8bdf5e0a update conda-pack-build.yml 2025-10-12 19:34:16 +08:00
Xuwznln
299f010754 update conda-pack-build.yml 2025-10-12 19:27:25 +08:00
Xuwznln
15ce0d6883 update conda-pack-build.yml 2025-10-12 19:04:48 +08:00
Xuwznln
dec474e1a7 add auto install script for conda-pack-build.yml
(cherry picked from commit 172599adcf)
2025-10-12 18:53:10 +08:00
Xuwznln
5f187899fc add conda-pack-build.yml 2025-10-12 17:27:59 +08:00
Xuwznln
c8d16c7024 update todo 2025-10-11 13:53:17 +08:00
Junhan Chang
25d46dc9d5 pass the tests 2025-10-11 07:20:34 +08:00
Junhan Chang
88c4d1a9d1 modify bioyond/plr converter, bioyond resource registry, and tests 2025-10-11 04:59:59 +08:00
Xuwznln
81fd8291c5 update todo 2025-10-11 03:38:59 +08:00
Xuwznln
3a11eb90d4 feat: 允许返回非本节点物料,后面可以通过decoration进行区分,就不进行warning了 2025-10-11 03:38:14 +08:00
Xuwznln
387866b9c9 修复同步任务报错不显示的bug 2025-10-11 03:14:12 +08:00
Xuwznln
7f40f141f6 移动内部action以兼容host node 2025-10-11 03:11:17 +08:00
Xuwznln
6fc7ed1b88 过滤本地动作 2025-10-11 03:06:37 +08:00
Xuwznln
93f0e08d75 fix host_node test_resource error 2025-10-11 03:04:15 +08:00
Xuwznln
4b43734b55 fix host_node test_resource error 2025-10-11 02:57:14 +08:00
Xuwznln
174b1914d4 fix host_node error 2025-10-11 02:54:15 +08:00
Xuwznln
704e13f030 新增test_resource动作 2025-10-11 02:53:18 +08:00
Xuwznln
0c42d60cf2 更新transfer_resource_to_another参数,支持spot入参 2025-10-11 02:41:37 +08:00
Xuwznln
df33e1a214 修复transfer_resource_to_another生成 2025-10-11 01:12:56 +08:00
Xuwznln
1f49924966 修复资源添加 2025-10-11 00:58:56 +08:00
Xuwznln
609b6006e8 支持选择器注册表自动生成
支持转运物料
2025-10-11 00:57:22 +08:00
Xuwznln
67c01271b7 add update remove 2025-10-10 20:15:16 +08:00
Xuwznln
a1783f489e Merge remote-tracking branch 'origin/workstation_dev_YB2' into dev
# Conflicts:
#	unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py
#	unilabos/devices/workstation/bioyond_studio/station.py
#	unilabos/resources/graphio.py
2025-10-10 15:38:45 +08:00
Xuwznln
a8f6527de9 修复to_plr_resources 2025-10-10 15:30:26 +08:00
ZiWei
54cfaf15f3 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.
2025-10-10 15:25:50 +08:00
Xuwznln
5610c28b67 更新物料接口 2025-10-10 07:13:59 +08:00
Junhan Chang
cfc1ee6e79 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>
2025-09-30 17:23:13 +08:00
Junhan Chang
1c9d2ee98a fix bioyond resource io 2025-09-30 17:02:38 +08:00
Junhan Chang
3fe8f4ca44 add child_size for itemized_carrier 2025-09-30 12:58:42 +08:00
Junhan Chang
2476821dcc update bioyond launch json 2025-09-30 12:25:21 +08:00
Junhan Chang
7b426ed5ae create warehouse by factory func 2025-09-30 11:57:34 +08:00
Junhan Chang
9bbae96447 Merge branch 'workstation_dev_YB2' of https://github.com/dptech-corp/Uni-Lab-OS into workstation_dev_YB2 2025-09-29 21:02:05 +08:00
Junhan Chang
10aabb7592 refactor: add itemized_carrier instead of carrier consists of ResourceHolder 2025-09-29 20:36:45 +08:00
Junhan Chang
709eb0d91c Merge branch 'dev' of https://github.com/dptech-corp/Uni-Lab-OS into dev 2025-09-27 00:10:59 +08:00
Junhan Chang
14b7d52825 create/update resources with POST/PUT for big amount/ small amount data 2025-09-26 23:25:50 +08:00
Junhan Chang
a5397ffe12 create/update resources with POST/PUT for big amount/ small amount data 2025-09-26 23:25:34 +08:00
LccLink
c6c2da69ba frontend_docs 2025-09-26 23:20:22 +08:00
Junhan Chang
622e579063 fix: update resource with correct structure; remove deprecated liquid_handler set_group action 2025-09-26 20:24:15 +08:00
Junhan Chang
196e0f7e2b fix bioyond station and registry 2025-09-26 08:12:41 +08:00
Junhan Chang
a632fd495e bioyond station with communication init and resource sync 2025-09-25 20:56:29 +08:00
Junhan Chang
a8cc02a126 add bioyond studio draft 2025-09-25 20:36:52 +08:00
Xie Qiming
ad2e1432c6 feat: 将新威电池测试系统驱动与配置文件并入 workstation_dev_YB2 (#92)
* feat: 新威电池测试系统驱动与注册文件

* feat: bring neware driver & battery.json into workstation_dev_YB2
2025-09-25 18:53:04 +08:00
Junhan Chang
c3b9583eac fix: update resource with correct structure; remove deprecated liquid_handler set_group action 2025-09-25 15:27:05 +08:00
Junhan Chang
5c47cd0c8a add BIOYOND deck assignment and pass all tests 2025-09-25 08:41:41 +08:00
Junhan Chang
63ab1af45d refactor and add BIOYOND resources tests 2025-09-25 08:14:48 +08:00
Junhan Chang
a8419dc0c3 add standardized BIOYOND resources: bottle_carrier, bottle 2025-09-25 03:49:07 +08:00
Junhan Chang
34f05f2e25 refactor: rename "station_resource" to "deck" 2025-09-24 10:53:11 +08:00
h840473807
0dc2488f02 coin_cell_station draft 2025-09-23 01:18:04 +08:00
Junhan Chang
f13156e792 fix dict to tree/nested-dict converter 2025-09-23 00:02:45 +08:00
Xuwznln
13fd1ac572 更新物料接口 2025-09-22 17:14:48 +08:00
Guangxin Zhang
f8ef6e0686 Add Defaultlayout 2025-09-19 19:34:25 +01:00
Xuwznln
94a7b8aaca Update install md 2025-09-19 23:02:46 +08:00
Xuwznln
301bea639e 修复protocolnode的兼容性 2025-09-19 22:54:27 +08:00
Xuwznln
4b5a83efa4 修复protocolnode的兼容性 2025-09-19 21:09:07 +08:00
Xuwznln
2889e9be2c 更新所有注册表 2025-09-19 20:28:43 +08:00
Xuwznln
304aebbba7 bump version to 0.10.6 2025-09-19 19:55:34 +08:00
Xuwznln
091c9fa247 Merge branch 'workstation_dev' into dev
# Conflicts:
#	.conda/recipe.yaml
#	recipes/msgs/recipe.yaml
#	recipes/unilabos/recipe.yaml
#	setup.py
#	unilabos/registry/devices/work_station.yaml
#	unilabos/ros/nodes/base_device_node.py
#	unilabos/ros/nodes/presets/protocol_node.py
#	unilabos_msgs/package.xml
2025-09-19 19:52:53 +08:00
Xuwznln
67ca45a240 remove class for resource 2025-09-19 19:33:28 +08:00
Xuwznln
7aab2ea493 fix resource download 2025-09-19 19:17:03 +08:00
Xuwznln
62f3a6d696 PRCXI9320 json 2025-09-19 17:14:43 +08:00
Xuwznln
eb70ad0e18 PRCXI9320 json 2025-09-19 16:52:12 +08:00
Xuwznln
768f43880e PRCXI9320 json 2025-09-19 16:29:18 +08:00
Xuwznln
762c3c737c 重新补全zhida注册表 2025-09-19 11:45:57 +08:00
Xie Qiming
ace98a4472 Feature/xprbalance-zhida (#80)
* feat(devices): add Zhida GC/MS pretreatment automation workstation

* feat(devices): add mettler_toledo xpr balance

* balance
2025-09-19 11:43:25 +08:00
Xuwznln
41eaa88c6f 修复移液站错误的aspirate注册表 2025-09-19 07:05:09 +08:00
Xuwznln
a1a55a2c0a fix resource_add 2025-09-19 06:25:28 +08:00
Xuwznln
2eaa0ca729 try fix add protocol 2025-09-19 06:21:29 +08:00
Xuwznln
6f8f070f40 fix protocol node log_message, added create_resource return value 2025-09-19 05:36:47 +08:00
Xuwznln
da4bd927e0 fix protocol node log_message, added create_resource return value 2025-09-19 05:31:49 +08:00
Xuwznln
01f8816597 update registry with nested obj 2025-09-19 03:44:18 +08:00
Guangxin Zhang
e5006285df 重新规定了版位推荐的入参 2025-09-18 15:27:22 +01:00
Guangxin Zhang
573c724a5c 新增版位推荐功能 2025-09-17 21:07:19 +01:00
Xuwznln
09549d2839 resource_update use resource_add 2025-09-18 03:47:26 +08:00
Junhan Chang
50c7777cea Fix: run-column with correct vessel id (#86)
* fix run_column

* Update run_column_protocol.py

(cherry picked from commit e5aa4d940a)
2025-09-16 14:40:16 +08:00
Xuwznln
4888f02c09 add server timeout 2025-09-16 09:47:06 +08:00
Xuwznln
779c9693d9 refactor ws client 2025-09-16 05:24:42 +08:00
Xuwznln
ffa841a41a fix dupe upload registry 2025-09-15 16:25:41 +08:00
Xuwznln
fc669f09f8 fix import error 2025-09-15 15:55:44 +08:00
Xuwznln
2ca0311de6 移除MQTT,更新launch文档,提供注册表示例文件,更新到0.10.5 2025-09-15 02:39:43 +08:00
Guangxin Zhang
94cdcbf24e 对于PRCXI9320的transfer_group,一对多和多对多 2025-09-15 00:29:16 +08:00
Xuwznln
1cd07915e7 Correct runze pump multiple receive method. 2025-09-14 03:17:50 +08:00
Xuwznln
b600fc666d Correct runze pump multiple receive method. 2025-09-14 03:07:48 +08:00
Xuwznln
9e214c56c1 Update runze_multiple_backbone 2025-09-14 01:04:50 +08:00
Xuwznln
bdf27a7e82 Correct runze multiple backbone 2025-09-14 00:40:29 +08:00
Xuwznln
2493fb9f94 Update runze pump format 2025-09-14 00:22:39 +08:00
Xuwznln
c7a0ff67a9 support multiple backbone
(cherry picked from commit 4771ff2347)
2025-09-14 00:21:54 +08:00
Xuwznln
711a7c65fa remove runze multiple software obtainer
(cherry picked from commit 8bcc92a394)
2025-09-14 00:21:53 +08:00
Xuwznln
cde7956896 runze multiple pump support
(cherry picked from commit 49354fcf39)
2025-09-14 00:21:52 +08:00
Xuwznln
95b6fd0451 新增uat的地址替换 2025-09-11 16:38:17 +08:00
Xuwznln
513e848d89 result_info改为字典类型 2025-09-11 16:24:53 +08:00
Guangxin Zhang
58d1cc4720 Add set_group and transfer_group methods to PRCXI9300Handler and update liquid_handler.yaml 2025-09-10 21:23:15 +08:00
Guangxin Zhang
5676dd6589 Add LiquidHandlerSetGroup and LiquidHandlerTransferGroup actions to CMakeLists 2025-09-10 20:57:22 +08:00
Guangxin Zhang
1ae274a833 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.
2025-09-10 20:57:16 +08:00
Xuwznln
22b88c8441 取消labid 和 强制config输入 2025-09-10 20:55:24 +08:00
Xuwznln
81bcc1907d fix: addr param 2025-09-10 20:14:33 +08:00
Xuwznln
8cffd3dc21 fix: addr param 2025-09-10 20:13:44 +08:00
Xuwznln
a722636938 增加addr参数 2025-09-10 20:01:10 +08:00
Xuwznln
f68340d932 修复status密集发送时,消息出错 2025-09-10 18:52:23 +08:00
Xuwznln
361eae2f6d 注册表编辑器 2025-09-07 20:57:48 +08:00
Xuwznln
c25283ae04 主机节点信息等支持自动刷新 2025-09-07 12:53:00 +08:00
Xuwznln
961752fb0d 更新schema的title字段 2025-09-07 00:43:23 +08:00
Xuwznln
55165024dd 修复async错误 2025-09-04 20:19:15 +08:00
Xuwznln
6ddceb8393 修复edge上报错误 2025-09-04 19:31:19 +08:00
Xuwznln
4e52c7d2f4 修复event loop错误 2025-09-04 17:11:50 +08:00
Xuwznln
0b56efc89d 增加handle检测,增加material edge关系上传 2025-09-04 16:46:25 +08:00
Xuwznln
a27b93396a 修复工站的tracker实例追踪失效问题 2025-09-04 02:51:13 +08:00
Xuwznln
2a60a6c27e 修正物料关系上传 2025-09-03 14:20:37 +08:00
Xuwznln
5dda94044d 增加物料关系上传日志 2025-09-03 12:31:25 +08:00
Xuwznln
0cfc6f45e3 增加物料关系上传日志 2025-09-03 12:20:54 +08:00
Xuwznln
831f4549f9 ws protocol 2025-09-02 18:51:27 +08:00
Xuwznln
f4d4eb06d3 ws test version 2 2025-09-02 18:29:05 +08:00
Xuwznln
e3b8164f6b ws test version 1 2025-09-02 14:32:02 +08:00
Xuwznln
78c04acc2e fix: missing job_id key 2025-09-01 16:34:23 +08:00
Xuwznln
cd0428ea78 fix: build 2025-08-30 12:24:28 +08:00
Xuwznln
bdddbd57ba fix: 还原protocol node处理方法 2025-08-30 12:22:46 +08:00
Xuwznln
a312de08a5 fix: station自己的方法注册错误 2025-08-30 12:20:24 +08:00
Xuwznln
68513b5745 feat: action status 2025-08-29 15:38:16 +08:00
Xuwznln
19027350fb feat: workstation example 2025-08-29 02:47:20 +08:00
Xuwznln
bbbdb06bbc feat: websocket test 2025-08-28 19:57:14 +08:00
Xuwznln
cd84e26126 feat: websocket 2025-08-28 14:34:38 +08:00
Junhan Chang
ce5bab3af1 example for use WorkstationBase 2025-08-27 15:20:20 +08:00
Junhan Chang
82d9ef6bf7 uncompleted refactor 2025-08-27 15:19:58 +08:00
Junhan Chang
332b33c6f4 simplify resource system 2025-08-27 11:13:56 +08:00
ZiWei
1ec642ee3a update: Workstation dev 将版本号从 0.10.3 更新为 0.10.4 (#84)
* Add:msgs.action

* update: 将版本号从 0.10.3 更新为 0.10.4
2025-08-27 01:55:28 +08:00
ZiWei
7d8e6d029b Add:msgs.action (#83) 2025-08-27 01:21:13 +08:00
Junhan Chang
5ec8a57a1f refactor: ProtocolNode→WorkstationNode 2025-08-25 22:09:37 +08:00
Junhan Chang
ae3c1100ae refactor: workstation_base 重构为仅含业务逻辑,通信和子设备管理交给 ProtocolNode 2025-08-22 06:43:43 +08:00
Junhan Chang
14bc2e6cda Create workstation_architecture.md 2025-08-21 10:09:57 +08:00
Junhan Chang
9f823a4198 update workstation base 2025-08-21 10:05:58 +08:00
Xuwznln
02c79363c1 feat: add sk & ak 2025-08-20 21:23:08 +08:00
Junhan Chang
227ff1284a add workstation template and battery example 2025-08-19 21:35:27 +08:00
Xuwznln
4b7bde6be5 Update recipe.yaml 2025-08-13 16:36:53 +08:00
Xuwznln
8a669ac35a fix: figure_resource 2025-08-13 13:23:02 +08:00
Junhan Chang
a1538da39e use call_async in all service to avoid deadlock 2025-08-13 04:25:51 +08:00
Xuwznln
0063df4cf3 fix: prcxi import error 2025-08-12 19:31:52 +08:00
Xuwznln
e570ba4976 临时兼容错误的driver写法 2025-08-12 19:20:53 +08:00
Xuwznln
e8c1f76dbb fix protocol node 2025-08-12 17:08:59 +08:00
Junhan Chang
f791c1a342 fix filter protocol 2025-08-12 16:48:32 +08:00
Junhan Chang
ea60cbe891 bugfixes on organic protocols 2025-08-12 14:50:01 +08:00
Junhan Chang
eac9b8ab3d fix and remove redundant info 2025-08-11 20:52:03 +08:00
Xuwznln
573bcf1a6c feat: 新增use_remote_resource参数 2025-08-11 16:09:27 +08:00
Junhan Chang
50e93cb1af fix all protocol_compilers and remove deprecated devices 2025-08-11 15:01:04 +08:00
Xuwznln
fe1a029a9b feat: 优化protocol node节点运行日志 2025-08-10 17:31:44 +08:00
Junhan Chang
662c063f50 fix pumps and liquid_handler handle 2025-08-07 20:59:57 +08:00
Xuwznln
01cbbba0b3 feat: workstation example 2025-08-07 15:26:17 +08:00
Xuwznln
e6c556cf19 add: prcxi res
fix: startup slow
2025-08-07 01:26:33 +08:00
Xuwznln
0605f305ed fix: prcxi_res 2025-08-06 23:06:22 +08:00
Xuwznln
37d8108ec4 fix: discard_tips 2025-08-06 19:27:10 +08:00
Xuwznln
6081dac561 fix: discard_tips error 2025-08-06 19:18:35 +08:00
Xuwznln
5b2d066127 fix: drop_tips not using auto resource select 2025-08-06 19:10:04 +08:00
ZiWei
06e66765e7 feat: 添加ChinWe设备控制类,支持串口通信和电机控制功能 (#79) 2025-08-06 18:49:37 +08:00
Xuwznln
98ce360088 feat: add trace log level 2025-08-04 20:27:02 +08:00
Xuwznln
5cd0f72fbd modify default discovery_interval to 15s 2025-08-04 14:10:43 +08:00
Xuwznln
343f394203 fix: working dir error when input config path
feat: report publish topic when error
2025-08-04 14:04:31 +08:00
Junhan Chang
46aa7a7bd2 fix: workstation handlers and vessel_id parsing 2025-08-04 10:24:42 +08:00
Junhan Chang
a66369e2c3 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>
2025-08-03 11:21:37 +08:00
45 changed files with 3943 additions and 7837 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 /C CALL {0}' || 'bash -el {0}' }} shell: ${{ matrix.platform == 'win-64' && 'cmd' || 'bash' }}
steps: steps:
- name: Check if platform should be built - name: Check if platform should be built
@@ -73,7 +73,6 @@ 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
@@ -82,7 +81,7 @@ jobs:
run: | run: |
echo Installing unilabos and dependencies to unilab environment... echo Installing unilabos and dependencies to unilab environment...
echo Using mamba for faster and more reliable dependency resolution... echo Using mamba for faster and more reliable dependency resolution...
mamba install uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y mamba install -n unilab uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
- name: Install conda-pack, unilabos and dependencies (Unix) - name: Install conda-pack, unilabos and dependencies (Unix)
if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64' if: steps.should_build.outputs.should_build == 'true' && matrix.platform != 'win-64'
@@ -90,15 +89,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 uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y mamba install -n unilab uni-lab::unilabos conda-pack -c uni-lab -c robostack-staging -c conda-forge -y
- name: Get latest ros-humble-unilabos-msgs version (Windows) - 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 ros-humble-unilabos-msgs conda list -n unilab ros-humble-unilabos-msgs
for /f "tokens=2" %%i in ('conda list ros-humble-unilabos-msgs --json ^| python -c "import sys, json; pkgs=json.load(sys.stdin); print(pkgs[0]['version'] if pkgs else 'not-found')"') do set VERSION=%%i for /f "tokens=2" %%i in ('conda list -n unilab ros-humble-unilabos-msgs --json ^| python -c "import sys, json; pkgs=json.load(sys.stdin); print(pkgs[0]['version'] if pkgs else 'not-found')"') do set VERSION=%%i
echo installed_version=%VERSION% >> %GITHUB_OUTPUT% echo installed_version=%VERSION% >> %GITHUB_OUTPUT%
echo Installed ros-humble-unilabos-msgs version: %VERSION% echo Installed ros-humble-unilabos-msgs version: %VERSION%
@@ -108,7 +107,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 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 -n unilab ros-humble-unilabos-msgs --json | python -c "import sys, json; pkgs=json.load(sys.stdin); print(pkgs[0]['version'] if pkgs else 'not-found')")
echo "installed_version=$VERSION" >> $GITHUB_OUTPUT echo "installed_version=$VERSION" >> $GITHUB_OUTPUT
echo "Installed ros-humble-unilabos-msgs version: $VERSION" echo "Installed ros-humble-unilabos-msgs version: $VERSION"
@@ -119,7 +118,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 ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge -y || echo Already at latest version mamba update -n unilab ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge -y || echo Already at latest version
- name: Check for newer ros-humble-unilabos-msgs (Unix) - 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'
@@ -129,65 +128,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 ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge -y || echo "Already at latest version" mamba update -n unilab ros-humble-unilabos-msgs -c uni-lab -c robostack-staging -c conda-forge -y || echo "Already at latest version"
- name: Install latest unilabos from source (Windows) - 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...
pip uninstall unilabos -y || echo unilabos not installed via pip mamba run -n unilab 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 }})...
pip install . mamba run -n unilab pip install .
echo Verifying installation... echo Verifying installation...
pip show unilabos mamba run -n unilab 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..."
pip uninstall unilabos -y || echo "unilabos not installed via pip" mamba run -n unilab 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 }})..."
pip install . mamba run -n unilab pip install .
echo "Verifying installation..." echo "Verifying installation..."
pip show unilabos mamba run -n unilab 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 ===
conda env list mamba env list
echo. echo.
echo === Installed Packages === echo === Installed Packages ===
conda list | findstr /C:"unilabos" /C:"ros-humble-unilabos-msgs" || conda list mamba list -n unilab | findstr /C:"unilabos" /C:"ros-humble-unilabos-msgs" || mamba list -n unilab
echo. echo.
echo === Python Packages === echo === Python Packages ===
pip list | findstr unilabos || pip list mamba run -n unilab pip list | findstr unilabos || mamba run -n unilab 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 ==="
conda env list mamba env list
echo "" echo ""
echo "=== Installed Packages ===" echo "=== Installed Packages ==="
conda list | grep -E "(unilabos|ros-humble-unilabos-msgs)" || conda list mamba list -n unilab | grep -E "(unilabos|ros-humble-unilabos-msgs)" || mamba list -n unilab
echo "" echo ""
echo "=== Python Packages ===" echo "=== Python Packages ==="
pip list | grep unilabos || pip list mamba run -n unilab pip list | grep unilabos || mamba run -n unilab 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...
python -c "import sys; print(f'Python version: {sys.version}')" mamba run -n unilab python -c "import sys; print(f'Python version: {sys.version}')"
echo Verifying unilabos import... echo Verifying unilabos import...
python -c "import unilabos; print(f'UniLabOS version: {unilabos.__version__}')" || echo Warning: Could not import unilabos mamba run -n unilab python -c "import unilabos; print(f'UniLabOS version: {unilabos.__version__}')" || echo Warning: Could not import unilabos
echo Checking critical packages... echo Checking critical packages...
python -c "import rclpy; print('ROS2 rclpy: OK')" mamba run -n unilab python -c "import rclpy; print('ROS2 rclpy: OK')"
echo Running comprehensive verification script... echo Running comprehensive verification script...
python scripts\verify_installation.py || echo Warning: Verification script reported issues mamba run -n unilab python scripts\verify_installation.py --auto-install || echo Warning: Verification script reported issues
echo Environment verification complete! echo Environment verification complete!
- name: Verify environment integrity (Unix) - name: Verify environment integrity (Unix)
@@ -195,20 +194,20 @@ jobs:
shell: bash shell: bash
run: | run: |
echo "Verifying Python version..." echo "Verifying Python version..."
python -c "import sys; print(f'Python version: {sys.version}')" mamba run -n unilab python -c "import sys; print(f'Python version: {sys.version}')"
echo "Verifying unilabos import..." echo "Verifying unilabos import..."
python -c "import unilabos; print(f'UniLabOS version: {unilabos.__version__}')" || echo "Warning: Could not import unilabos" mamba run -n unilab python -c "import unilabos; print(f'UniLabOS version: {unilabos.__version__}')" || echo "Warning: Could not import unilabos"
echo "Checking critical packages..." echo "Checking critical packages..."
python -c "import rclpy; print('ROS2 rclpy: OK')" mamba run -n unilab python -c "import rclpy; print('ROS2 rclpy: OK')"
echo "Running comprehensive verification script..." echo "Running comprehensive verification script..."
python scripts/verify_installation.py || echo "Warning: Verification script reported issues" mamba run -n unilab python scripts/verify_installation.py --auto-install || 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...
conda pack -n unilab -o unilab-env-${{ matrix.platform }}.tar.gz --ignore-missing-files mamba activate unilab && 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
@@ -217,6 +216,7 @@ 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,6 +242,10 @@ 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
@@ -274,6 +278,10 @@ 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
@@ -283,46 +291,6 @@ 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
@@ -365,12 +333,8 @@ 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 " - Windows/macOS: Extract ZIP, then: tar -xzf unilab-pack-${{ matrix.platform }}.tar.gz" echo " install_unilab.sh"
echo " - Linux: Extract ZIP (or download tar.gz directly), run install_unilab.sh"
echo "==========================================" echo "=========================================="

View File

@@ -39,24 +39,39 @@ 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 Python environment - name: Setup Miniforge (with mamba)
uses: actions/setup-python@v5 uses: conda-incubator/setup-miniconda@v3
with: with:
python-version: '3.10' miniforge-version: latest
use-mamba: true
python-version: '3.11.11'
channels: conda-forge,robostack-staging,uni-lab,defaults
channel-priority: flexible
activate-environment: unilab
auto-update-conda: false
show-channel-urls: true
- name: Install system dependencies - name: Install unilabos and dependencies
run: | run: |
sudo apt-get update echo "Installing unilabos and dependencies to unilab environment..."
sudo apt-get install -y pandoc echo "Using mamba for faster and more reliable dependency resolution..."
mamba install -n unilab uni-lab::unilabos -c uni-lab -c robostack-staging -c conda-forge -y
- name: Install Python dependencies - name: Install latest unilabos from source
run: | run: |
python -m pip install --upgrade pip echo "Uninstalling existing unilabos..."
# Install package in development mode to get version info mamba run -n unilab pip uninstall unilabos -y || echo "unilabos not installed via pip"
pip install -e . echo "Installing unilabos from source..."
# Install documentation dependencies mamba run -n unilab pip install .
pip install -r docs/requirements.txt echo "Verifying installation..."
mamba run -n unilab pip show unilabos
- name: Install documentation dependencies
run: |
echo "Installing documentation build dependencies..."
mamba run -n unilab pip install -r docs/requirements.txt
- name: Setup Pages - name: Setup Pages
id: pages id: pages
@@ -68,8 +83,8 @@ jobs:
cd docs cd docs
# Clean previous builds # Clean previous builds
rm -rf _build rm -rf _build
# Build HTML documentation # Build HTML documentation in conda environment
python -m sphinx -b html . _build/html -v mamba run -n unilab 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://dptech-corp.github.io/Uni-Lab-OS/) - [Online Documentation](https://xuwznln.github.io/Uni-Lab-OS-Doc/)
## 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://dptech-corp.github.io/Uni-Lab-OS/boot_examples/index.html) Please refer to [Documentation - Boot Examples](https://xuwznln.github.io/Uni-Lab-OS-Doc/boot_examples/index.html)
## Message Format ## Message Format

View File

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

View File

@@ -91,7 +91,7 @@
使用以下命令启动模拟反应器: 使用以下命令启动模拟反应器:
```bash ```bash
unilab -g test/experiments/mock_reactor.json --app_bridges "" unilab -g test/experiments/mock_reactor.json
``` ```
### 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

@@ -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,7 +8,10 @@ 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 python verify_installation.py [--auto-install]
Options:
--auto-install Automatically install missing packages
Or in the conda environment: Or in the conda environment:
conda activate unilab conda activate unilab
@@ -17,14 +20,15 @@ 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") sys.stdout.reconfigure(encoding="utf-8", errors="replace") # type: ignore
sys.stderr.reconfigure(encoding="utf-8", errors="replace") sys.stderr.reconfigure(encoding="utf-8", errors="replace") # type: ignore
except (AttributeError, OSError): except (AttributeError, OSError):
pass pass
@@ -49,7 +53,7 @@ CHECK_MARK = "[OK]"
CROSS_MARK = "[FAIL]" CROSS_MARK = "[FAIL]"
def check_package(package_name: str, display_name: str = None) -> bool: def check_package(package_name: str, display_name: str | None = None) -> bool:
""" """
Check if a package can be imported. Check if a package can be imported.
@@ -87,9 +91,25 @@ 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
@@ -113,14 +133,16 @@ def main():
print(f" {CHECK_MARK} UniLabOS installed") print(f" {CHECK_MARK} UniLabOS installed")
# Check environment without auto-install (verification only) # Check environment with optional auto-install
# 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=False, show_details=False) env_check_passed = check_environment(auto_install=args.auto_install, 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

@@ -170,15 +170,16 @@
"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": [ "liquids": [["DMF", 500.0]],
{ "pending_liquids": [["DMF", 500.0]]
"liquid_type": "DMF",
"liquid_volume": 1000.0
}
]
} }
}, },
{ {
@@ -194,15 +195,16 @@
"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": [ "liquids": [["ethyl_acetate", 1000.0]],
{ "pending_liquids": [["ethyl_acetate", 1000.0]]
"liquid_type": "ethyl_acetate",
"liquid_volume": 1000.0
}
]
} }
}, },
{ {
@@ -218,15 +220,16 @@
"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": [ "liquids": [["hexane", 1000.0]],
{ "pending_liquids": [["hexane", 1000.0]]
"liquid_type": "hexane",
"liquid_volume": 1000.0
}
]
} }
}, },
{ {
@@ -242,15 +245,16 @@
"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": [ "liquids": [["methanol", 1000.0]],
{ "pending_liquids": [["methanol", 1000.0]]
"liquid_type": "methanol",
"liquid_volume": 1000.0
}
]
} }
}, },
{ {
@@ -266,15 +270,16 @@
"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": [ "liquids": [["water", 1000.0]],
{ "pending_liquids": [["water", 1000.0]]
"liquid_type": "water",
"liquid_volume": 1000.0
}
]
} }
}, },
{ {
@@ -335,14 +340,16 @@
}, },
"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": []
} }
}, },
{ {
@@ -419,11 +426,16 @@
"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": []
} }
}, },
{ {
@@ -439,11 +451,16 @@
"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": []
} }
}, },
{ {
@@ -649,11 +666,16 @@
"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": []
} }
}, },
{ {
@@ -669,11 +691,16 @@
"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": []
} }
}, },
{ {
@@ -689,11 +716,16 @@
"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": []
} }
}, },
{ {
@@ -733,6 +765,11 @@
}, },
"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"
}, },
@@ -756,6 +793,11 @@
}, },
"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"
}, },
@@ -779,6 +821,11 @@
}, },
"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": "dispensing_station.bioyond", "class": "workstation.bioyond_dispensing_station",
"config": { "config": {
"config": { "config": {
"api_key": "DE9BDDA0", "api_key": "DE9BDDA0",
@@ -20,13 +20,6 @@
"_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": {}

View File

@@ -10,7 +10,7 @@
"type": "device", "type": "device",
"class": "reaction_station.bioyond", "class": "reaction_station.bioyond",
"config": { "config": {
"bioyond_config": { "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,14 +19,18 @@
"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)": "3a160824-0665-01ed-285a-51ef817a9046", "Liquid_feeding(titration)": "3a16082a-96ac-0449-446a-4ed39f3365b6",
"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": {
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier", "烧杯": ["BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"],
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier", "试剂瓶": ["BIOYOND_PolymerStation_1BottleCarrier", ""],
"样品板": "BIOYOND_PolymerStation_6VialCarrier" "样品板": ["BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"],
"分装板": ["BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"],
"样品瓶": ["BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"],
"90%分装小瓶": ["BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"],
"10%分装小瓶": ["BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"]
} }
}, },
"deck": { "deck": {
@@ -42,7 +46,6 @@
{ {
"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

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

@@ -0,0 +1,115 @@
#!/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

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

View File

@@ -11,18 +11,14 @@ 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.config.config import load_config, BasicConfig, HTTPConfig
from unilabos.utils.banner_print import print_status, print_unilab_banner from unilabos.utils.banner_print import print_status, print_unilab_banner
from unilabos.resources.graphio import modify_to_backend_format from unilabos.config.config import load_config, BasicConfig, HTTPConfig
def load_config_from_file(config_path): def load_config_from_file(config_path):
if config_path is None: if config_path is None:
@@ -184,6 +180,7 @@ 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):
@@ -215,6 +212,14 @@ 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"
@@ -268,6 +273,8 @@ 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)
@@ -349,7 +356,7 @@ def main():
if BasicConfig.upload_registry: if BasicConfig.upload_registry:
# 设备注册到服务端 - 需要 ak 和 sk # 设备注册到服务端 - 需要 ak 和 sk
if args_dict.get("ak") and args_dict.get("sk"): if BasicConfig.ak and BasicConfig.sk:
print_status("开始注册设备到服务端...", "info") print_status("开始注册设备到服务端...", "info")
try: try:
register_devices_and_resources(lab_registry) register_devices_and_resources(lab_registry)

View File

@@ -73,6 +73,8 @@ 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:
@@ -92,6 +94,8 @@ class HTTPClient:
timeout=100, 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:

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 from typing import Optional, Literal
from unilabos.utils import logger from unilabos.utils import logger
@@ -18,6 +18,7 @@ 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

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

File diff suppressed because it is too large Load Diff

View File

@@ -9,22 +9,6 @@ API_CONFIG = {
"api_host": "" "api_host": ""
} }
# 站点类型配置
STATION_TYPES = {
"REACTION": "reaction_station", # 仅反应站
"DISPENSING": "dispensing_station", # 仅配液站
"HYBRID": "hybrid_station" # 混合模式
}
# 默认站点配置
DEFAULT_STATION_CONFIG = {
"station_type": STATION_TYPES["REACTION"], # 默认反应站模式
"enable_reaction_station": True, # 是否启用反应站功能
"enable_dispensing_station": False, # 是否启用配液站功能
"station_name": "BioyondReactionStation", # 站点名称
"description": "Bioyond反应工作站" # 站点描述
}
# 工作流映射配置 # 工作流映射配置
WORKFLOW_MAPPINGS = { WORKFLOW_MAPPINGS = {
"reactor_taken_out": "", "reactor_taken_out": "",
@@ -49,52 +33,75 @@ WORKFLOW_TO_SECTION_MAP = {
} }
# 库位映射配置 # 库位映射配置
LOCATION_MAPPING = { WAREHOUSE_MAPPING = {
'A01': '', "粉末堆栈": {
'A02': '', "uuid": "",
'A03': '', "site_uuids": {
'A04': '', # 样品板
'A05': '', "A1": "3a14198e-6929-31f0-8a22-0f98f72260df",
'A06': '', "A2": "3a14198e-6929-4379-affa-9a2935c17f99",
'A07': '', "A3": "3a14198e-6929-56da-9a1c-7f5fbd4ae8af",
'A08': '', "A4": "3a14198e-6929-5e99-2b79-80720f7cfb54",
'B01': '', "B1": "3a14198e-6929-f525-9a1b-1857552b28ee",
'B02': '', "B2": "3a14198e-6929-bf98-0fd5-26e1d68bf62d",
'B03': '', "B3": "3a14198e-6929-2d86-a468-602175a2b5aa",
'B04': '', "B4": "3a14198e-6929-1a98-ae57-e97660c489ad",
'B05': '', # 分装板
'B06': '', "C1": "3a14198e-6929-46fe-841e-03dd753f1e4a",
'B07': '', "C2": "3a14198e-6929-1bc9-a9bd-3b7ca66e7f95",
'B08': '', "C3": "3a14198e-6929-72ac-32ce-9b50245682b8",
'C01': '', "C4": "3a14198e-6929-3bd8-e6c7-4a9fd93be118",
'C02': '', "D1": "3a14198e-6929-8a0b-b686-6f4a2955c4e2",
'C03': '', "D2": "3a14198e-6929-dde1-fc78-34a84b71afdf",
'C04': '', "D3": "3a14198e-6929-a0ec-5f15-c0f9f339f963",
'C05': '', "D4": "3a14198e-6929-7ac8-915a-fea51cb2e884"
'C06': '', }
'C07': '', },
'C08': '', "溶液堆栈": {
'D01': '', "uuid": "",
'D02': '', "site_uuids": {
'D03': '', "A1": "3a14198e-d724-e036-afdc-2ae39a7f3383",
'D04': '', "A2": "3a14198e-d724-afa4-fc82-0ac8a9016791",
'D05': '', "A3": "3a14198e-d724-ca48-bb9e-7e85751e55b6",
'D06': '', "A4": "3a14198e-d724-df6d-5e32-5483b3cab583",
'D07': '', "B1": "3a14198e-d724-d818-6d4f-5725191a24b5",
'D08': '', "B2": "3a14198e-d724-be8a-5e0b-012675e195c6",
"B3": "3a14198e-d724-cc1e-5c2c-228a130f40a8",
"B4": "3a14198e-d724-1e28-c885-574c3df468d0",
"C1": "3a14198e-d724-b5bb-adf3-4c5a0da6fb31",
"C2": "3a14198e-d724-ab4e-48cb-817c3c146707",
"C3": "3a14198e-d724-7f18-1853-39d0c62e1d33",
"C4": "3a14198e-d724-28a2-a760-baa896f46b66",
"D1": "3a14198e-d724-d378-d266-2508a224a19f",
"D2": "3a14198e-d724-f56e-468b-0110a8feb36a",
"D3": "3a14198e-d724-0cf1-dea9-a1f40fe7e13c",
"D4": "3a14198e-d724-0ddd-9654-f9352a421de9"
}
},
"试剂堆栈": {
"uuid": "",
"site_uuids": {
"A1": "3a14198c-c2cf-8b40-af28-b467808f1c36",
"A2": "3a14198c-c2d0-f3e7-871a-e470d144296f",
"A3": "3a14198c-c2d0-dc7d-b8d0-e1d88cee3094",
"A4": "3a14198c-c2d0-2070-efc8-44e245f10c6f",
"B1": "3a14198c-c2d0-354f-39ad-642e1a72fcb8",
"B2": "3a14198c-c2d0-1559-105d-0ea30682cab4",
"B3": "3a14198c-c2d0-725e-523d-34c037ac2440",
"B4": "3a14198c-c2d0-efce-0939-69ca5a7dfd39"
}
}
} }
# 物料类型配置 # 物料类型配置
MATERIAL_TYPE_IDS = {
"样品板": "",
"样品": "",
"烧杯": ""
}
MATERIAL_TYPE_MAPPINGS = { MATERIAL_TYPE_MAPPINGS = {
"烧杯": "BIOYOND_PolymerStation_1FlaskCarrier", "烧杯": ("BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"),
"试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier", "试剂瓶": ("BIOYOND_PolymerStation_1BottleCarrier", ""),
"样品板": "BIOYOND_PolymerStation_6VialCarrier", "样品板": ("BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"),
"分装板": ("BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"),
"样品瓶": ("BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"),
"90%分装小瓶": ("BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"),
"10%分装小瓶": ("BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"),
} }
# 步骤参数配置各工作流的步骤UUID # 步骤参数配置各工作流的步骤UUID
@@ -127,3 +134,5 @@ WORKFLOW_STEP_IDS = {
"observe": "" "observe": ""
} }
} }
LOCATION_MAPPING = {}

View File

@@ -0,0 +1,831 @@
from datetime import datetime
import json
from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondException
from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstation
class BioyondDispensingStation(BioyondWorkstation):
def __init__(
self,
config,
# 桌子
deck,
*args,
**kwargs,
):
super().__init__(config, deck, *args, **kwargs)
# self.config = config
# self.api_key = config["api_key"]
# self.host = config["api_host"]
#
# # 使用简单的Logger替代原来的logger
# self._logger = SimpleLogger()
# self.is_running = False
# 90%10%小瓶投料任务创建方法
def create_90_10_vial_feeding_task(self,
order_name: str = None,
speed: str = None,
temperature: str = None,
delay_time: str = None,
percent_90_1_assign_material_name: str = None,
percent_90_1_target_weigh: str = None,
percent_90_2_assign_material_name: str = None,
percent_90_2_target_weigh: str = None,
percent_90_3_assign_material_name: str = None,
percent_90_3_target_weigh: str = None,
percent_10_1_assign_material_name: str = None,
percent_10_1_target_weigh: str = None,
percent_10_1_volume: str = None,
percent_10_1_liquid_material_name: str = None,
percent_10_2_assign_material_name: str = None,
percent_10_2_target_weigh: str = None,
percent_10_2_volume: str = None,
percent_10_2_liquid_material_name: str = None,
percent_10_3_assign_material_name: str = None,
percent_10_3_target_weigh: str = None,
percent_10_3_volume: str = None,
percent_10_3_liquid_material_name: str = None,
hold_m_name: str = None) -> dict:
"""
创建90%10%小瓶投料任务
参数说明:
- order_name: 任务名称如果为None则使用默认名称
- speed: 搅拌速度如果为None则使用默认值400
- temperature: 温度如果为None则使用默认值40
- delay_time: 延迟时间如果为None则使用默认值600
- percent_90_1_assign_material_name: 90%_1物料名称
- percent_90_1_target_weigh: 90%_1目标重量
- percent_90_2_assign_material_name: 90%_2物料名称
- percent_90_2_target_weigh: 90%_2目标重量
- percent_90_3_assign_material_name: 90%_3物料名称
- percent_90_3_target_weigh: 90%_3目标重量
- percent_10_1_assign_material_name: 10%_1固体物料名称
- percent_10_1_target_weigh: 10%_1固体目标重量
- percent_10_1_volume: 10%_1液体体积
- percent_10_1_liquid_material_name: 10%_1液体物料名称
- percent_10_2_assign_material_name: 10%_2固体物料名称
- percent_10_2_target_weigh: 10%_2固体目标重量
- percent_10_2_volume: 10%_2液体体积
- percent_10_2_liquid_material_name: 10%_2液体物料名称
- percent_10_3_assign_material_name: 10%_3固体物料名称
- percent_10_3_target_weigh: 10%_3固体目标重量
- percent_10_3_volume: 10%_3液体体积
- percent_10_3_liquid_material_name: 10%_3液体物料名称
- hold_m_name: 库位名称,如"C01"用于查找对应的holdMId
返回: 任务创建结果
异常:
- BioyondException: 各种错误情况下的统一异常
"""
try:
# 1. 参数验证
if not hold_m_name:
raise BioyondException("hold_m_name 是必填参数")
# 检查90%物料参数的完整性
# 90%_1物料如果有物料名称或目标重量就必须有全部参数
if percent_90_1_assign_material_name or percent_90_1_target_weigh:
if not percent_90_1_assign_material_name:
raise BioyondException("90%_1物料如果提供了目标重量必须同时提供物料名称")
if not percent_90_1_target_weigh:
raise BioyondException("90%_1物料如果提供了物料名称必须同时提供目标重量")
# 90%_2物料如果有物料名称或目标重量就必须有全部参数
if percent_90_2_assign_material_name or percent_90_2_target_weigh:
if not percent_90_2_assign_material_name:
raise BioyondException("90%_2物料如果提供了目标重量必须同时提供物料名称")
if not percent_90_2_target_weigh:
raise BioyondException("90%_2物料如果提供了物料名称必须同时提供目标重量")
# 90%_3物料如果有物料名称或目标重量就必须有全部参数
if percent_90_3_assign_material_name or percent_90_3_target_weigh:
if not percent_90_3_assign_material_name:
raise BioyondException("90%_3物料如果提供了目标重量必须同时提供物料名称")
if not percent_90_3_target_weigh:
raise BioyondException("90%_3物料如果提供了物料名称必须同时提供目标重量")
# 检查10%物料参数的完整性
# 10%_1物料如果有物料名称、目标重量、体积或液体物料名称中的任何一个就必须有全部参数
if any([percent_10_1_assign_material_name, percent_10_1_target_weigh, percent_10_1_volume, percent_10_1_liquid_material_name]):
if not percent_10_1_assign_material_name:
raise BioyondException("10%_1物料如果提供了其他参数必须同时提供固体物料名称")
if not percent_10_1_target_weigh:
raise BioyondException("10%_1物料如果提供了其他参数必须同时提供固体目标重量")
if not percent_10_1_volume:
raise BioyondException("10%_1物料如果提供了其他参数必须同时提供液体体积")
if not percent_10_1_liquid_material_name:
raise BioyondException("10%_1物料如果提供了其他参数必须同时提供液体物料名称")
# 10%_2物料如果有物料名称、目标重量、体积或液体物料名称中的任何一个就必须有全部参数
if any([percent_10_2_assign_material_name, percent_10_2_target_weigh, percent_10_2_volume, percent_10_2_liquid_material_name]):
if not percent_10_2_assign_material_name:
raise BioyondException("10%_2物料如果提供了其他参数必须同时提供固体物料名称")
if not percent_10_2_target_weigh:
raise BioyondException("10%_2物料如果提供了其他参数必须同时提供固体目标重量")
if not percent_10_2_volume:
raise BioyondException("10%_2物料如果提供了其他参数必须同时提供液体体积")
if not percent_10_2_liquid_material_name:
raise BioyondException("10%_2物料如果提供了其他参数必须同时提供液体物料名称")
# 10%_3物料如果有物料名称、目标重量、体积或液体物料名称中的任何一个就必须有全部参数
if any([percent_10_3_assign_material_name, percent_10_3_target_weigh, percent_10_3_volume, percent_10_3_liquid_material_name]):
if not percent_10_3_assign_material_name:
raise BioyondException("10%_3物料如果提供了其他参数必须同时提供固体物料名称")
if not percent_10_3_target_weigh:
raise BioyondException("10%_3物料如果提供了其他参数必须同时提供固体目标重量")
if not percent_10_3_volume:
raise BioyondException("10%_3物料如果提供了其他参数必须同时提供液体体积")
if not percent_10_3_liquid_material_name:
raise BioyondException("10%_3物料如果提供了其他参数必须同时提供液体物料名称")
# 2. 生成任务编码和设置默认值
order_code = "task_vial_" + str(int(datetime.now().timestamp()))
if order_name is None:
order_name = "90%10%小瓶投料任务"
if speed is None:
speed = "400"
if temperature is None:
temperature = "40"
if delay_time is None:
delay_time = "600"
# 3. 工作流ID
workflow_id = "3a19310d-16b9-9d81-b109-0748e953694b"
# 4. 查询工作流对应的holdMID
material_info = self.hardware_interface.material_id_query(workflow_id)
if not material_info:
raise BioyondException(f"无法查询工作流 {workflow_id} 的物料信息")
# 获取locations列表
locations = material_info.get("locations", []) if isinstance(material_info, dict) else []
if not locations:
raise BioyondException(f"工作流 {workflow_id} 没有找到库位信息")
# 查找指定名称的库位
hold_mid = None
for location in locations:
if location.get("holdMName") == hold_m_name:
hold_mid = location.get("holdMId")
break
if not hold_mid:
raise BioyondException(f"未找到库位名称为 {hold_m_name} 的库位,请检查名称是否正确")
extend_properties = f"{{\"{ hold_mid }\": {{}}}}"
self.hardware_interface._logger.info(f"找到库位 {hold_m_name} 对应的holdMId: {hold_mid}")
# 5. 构建任务参数
order_data = {
"orderCode": order_code,
"orderName": order_name,
"workflowId": workflow_id,
"borderNumber": 1,
"paramValues": {},
"ExtendProperties": extend_properties
}
# 添加搅拌参数
order_data["paramValues"]["e8264e47-c319-d9d9-8676-4dd5cb382b11"] = [
{"m": 0, "n": 3, "Key": "speed", "Value": speed},
{"m": 0, "n": 3, "Key": "temperature", "Value": temperature}
]
# 添加延迟时间参数
order_data["paramValues"]["dc5dba79-5e4b-8eae-cbc5-e93482e43b1f"] = [
{"m": 0, "n": 4, "Key": "DelayTime", "Value": delay_time}
]
# 添加90%_1参数
if percent_90_1_assign_material_name is not None and percent_90_1_target_weigh is not None:
order_data["paramValues"]["e7d3c0a3-25c2-c42d-c84b-860c4a5ef844"] = [
{"m": 15, "n": 1, "Key": "targetWeigh", "Value": percent_90_1_target_weigh},
{"m": 15, "n": 1, "Key": "assignMaterialName", "Value": percent_90_1_assign_material_name}
]
# 添加90%_2参数
if percent_90_2_assign_material_name is not None and percent_90_2_target_weigh is not None:
order_data["paramValues"]["50b912c4-6c81-0734-1c8b-532428b2a4a5"] = [
{"m": 18, "n": 1, "Key": "targetWeigh", "Value": percent_90_2_target_weigh},
{"m": 18, "n": 1, "Key": "assignMaterialName", "Value": percent_90_2_assign_material_name}
]
# 添加90%_3参数
if percent_90_3_assign_material_name is not None and percent_90_3_target_weigh is not None:
order_data["paramValues"]["9c3674b3-c7cb-946e-fa03-fa2861d8aec4"] = [
{"m": 21, "n": 1, "Key": "targetWeigh", "Value": percent_90_3_target_weigh},
{"m": 21, "n": 1, "Key": "assignMaterialName", "Value": percent_90_3_assign_material_name}
]
# 添加10%_1固体参数
if percent_10_1_assign_material_name is not None and percent_10_1_target_weigh is not None:
order_data["paramValues"]["73a0bfd8-1967-45e9-4bab-c07ccd1a2727"] = [
{"m": 3, "n": 1, "Key": "targetWeigh", "Value": percent_10_1_target_weigh},
{"m": 3, "n": 1, "Key": "assignMaterialName", "Value": percent_10_1_assign_material_name}
]
# 添加10%_1液体参数
if percent_10_1_liquid_material_name is not None and percent_10_1_volume is not None:
order_data["paramValues"]["39634d40-c623-473a-8e5f-bc301aca2522"] = [
{"m": 3, "n": 3, "Key": "volume", "Value": percent_10_1_volume},
{"m": 3, "n": 3, "Key": "assignMaterialName", "Value": percent_10_1_liquid_material_name}
]
# 添加10%_2固体参数
if percent_10_2_assign_material_name is not None and percent_10_2_target_weigh is not None:
order_data["paramValues"]["2d9c16fa-2a19-cd47-a67b-3cadff9e3e3d"] = [
{"m": 7, "n": 1, "Key": "targetWeigh", "Value": percent_10_2_target_weigh},
{"m": 7, "n": 1, "Key": "assignMaterialName", "Value": percent_10_2_assign_material_name}
]
# 添加10%_2液体参数
if percent_10_2_liquid_material_name is not None and percent_10_2_volume is not None:
order_data["paramValues"]["e60541bb-ed68-e839-7305-2b4abe38a13d"] = [
{"m": 7, "n": 3, "Key": "volume", "Value": percent_10_2_volume},
{"m": 7, "n": 3, "Key": "assignMaterialName", "Value": percent_10_2_liquid_material_name}
]
# 添加10%_3固体参数
if percent_10_3_assign_material_name is not None and percent_10_3_target_weigh is not None:
order_data["paramValues"]["27494733-0f71-a916-7cd2-1929a0125f17"] = [
{"m": 11, "n": 1, "Key": "targetWeigh", "Value": percent_10_3_target_weigh},
{"m": 11, "n": 1, "Key": "assignMaterialName", "Value": percent_10_3_assign_material_name}
]
# 添加10%_3液体参数
if percent_10_3_liquid_material_name is not None and percent_10_3_volume is not None:
order_data["paramValues"]["c8798c29-786f-6858-7d7f-5330b890f2a6"] = [
{"m": 11, "n": 3, "Key": "volume", "Value": percent_10_3_volume},
{"m": 11, "n": 3, "Key": "assignMaterialName", "Value": percent_10_3_liquid_material_name}
]
# 6. 转换为JSON字符串并创建任务
json_str = json.dumps([order_data], ensure_ascii=False)
self.hardware_interface._logger.info(f"创建90%10%小瓶投料任务参数: {json_str}")
# 7. 调用create_order方法创建任务
result = self.hardware_interface.create_order(json_str)
self.hardware_interface._logger.info(f"创建90%10%小瓶投料任务结果: {result}")
return json.dumps({"suc": True})
except BioyondException:
# 重新抛出BioyondException
raise
except Exception as e:
# 捕获其他未预期的异常转换为BioyondException
error_msg = f"创建90%10%小瓶投料任务时发生未预期的错误: {str(e)}"
self.hardware_interface._logger.error(error_msg)
raise BioyondException(error_msg)
# 二胺溶液配置任务创建方法
def create_diamine_solution_task(self,
order_name: str = None,
material_name: str = None,
target_weigh: str = None,
volume: str = None,
liquid_material_name: str = "NMP",
speed: str = None,
temperature: str = None,
delay_time: str = None,
hold_m_name: str = None) -> dict:
"""
创建二胺溶液配置任务
参数说明:
- order_name: 任务名称如果为None则使用默认名称
- material_name: 固体物料名称,必填
- target_weigh: 固体目标重量,必填
- volume: 液体体积,必填
- liquid_material_name: 液体物料名称默认为NMP
- speed: 搅拌速度如果为None则使用默认值400
- temperature: 温度如果为None则使用默认值20
- delay_time: 延迟时间如果为None则使用默认值600
- hold_m_name: 库位名称,如"ODA-1"用于查找对应的holdMId
返回: 任务创建结果
异常:
- BioyondException: 各种错误情况下的统一异常
"""
try:
# 1. 参数验证
if not material_name:
raise BioyondException("material_name 是必填参数")
if not target_weigh:
raise BioyondException("target_weigh 是必填参数")
if not volume:
raise BioyondException("volume 是必填参数")
if not hold_m_name:
raise BioyondException("hold_m_name 是必填参数")
# 2. 生成任务编码和设置默认值
order_code = "task_oda_" + str(int(datetime.now().timestamp()))
if order_name is None:
order_name = f"二胺溶液配置-{material_name}"
if speed is None:
speed = "400"
if temperature is None:
temperature = "20"
if delay_time is None:
delay_time = "600"
# 3. 工作流ID - 二胺溶液配置工作流
workflow_id = "3a15d4a1-3bbe-76f9-a458-292896a338f5"
# 4. 查询工作流对应的holdMID
material_info = self.material_id_query(workflow_id)
if not material_info:
raise BioyondException(f"无法查询工作流 {workflow_id} 的物料信息")
# 获取locations列表
locations = material_info.get("locations", []) if isinstance(material_info, dict) else []
if not locations:
raise BioyondException(f"工作流 {workflow_id} 没有找到库位信息")
# 查找指定名称的库位
hold_mid = None
for location in locations:
if location.get("holdMName") == hold_m_name:
hold_mid = location.get("holdMId")
break
if not hold_mid:
raise BioyondException(f"未找到库位名称为 {hold_m_name} 的库位,请检查名称是否正确")
extend_properties = f"{{\"{ hold_mid }\": {{}}}}"
self.hardware_interface._logger.info(f"找到库位 {hold_m_name} 对应的holdMId: {hold_mid}")
# 5. 构建任务参数
order_data = {
"orderCode": order_code,
"orderName": order_name,
"workflowId": workflow_id,
"borderNumber": 1,
"paramValues": {
# 固体物料参数
"3a15d4a1-3bde-f5bc-053f-1ae0bf1f357e": [
{"m": 3, "n": 2, "Key": "targetWeigh", "Value": target_weigh},
{"m": 3, "n": 2, "Key": "assignMaterialName", "Value": material_name}
],
# 液体物料参数
"3a15d4a1-3bde-d584-b309-e661ae8f1c01": [
{"m": 3, "n": 3, "Key": "volume", "Value": volume},
{"m": 3, "n": 3, "Key": "assignMaterialName", "Value": liquid_material_name}
],
# 搅拌参数
"3a15d4a1-3bde-8ec4-1ced-92efc97ed73d": [
{"m": 3, "n": 6, "Key": "speed", "Value": speed},
{"m": 3, "n": 6, "Key": "temperature", "Value": temperature}
],
# 延迟时间参数
"3a15d4a1-3bde-3b92-83ff-8923a0addbbc": [
{"m": 3, "n": 7, "Key": "DelayTime", "Value": delay_time}
]
},
"ExtendProperties": extend_properties
}
# 6. 转换为JSON字符串并创建任务
json_str = json.dumps([order_data], ensure_ascii=False)
self.hardware_interface._logger.info(f"创建二胺溶液配置任务参数: {json_str}")
# 7. 调用create_order方法创建任务
result = self.hardware_interface.create_order(json_str)
self.hardware_interface._logger.info(f"创建二胺溶液配置任务结果: {result}")
return json.dumps({"suc": True})
except BioyondException:
# 重新抛出BioyondException
raise
except Exception as e:
# 捕获其他未预期的异常转换为BioyondException
error_msg = f"创建二胺溶液配置任务时发生未预期的错误: {str(e)}"
self.hardware_interface._logger.error(error_msg)
raise BioyondException(error_msg)
if __name__ == "__main__":
bioyond = BioyondDispensingStation(config={
"api_key": "DE9BDDA0",
"api_host": "http://192.168.1.200:44388"
})
# 示例1使用material_id_query查询工作流对应的holdMID
workflow_id_1 = "3a15d4a1-3bbe-76f9-a458-292896a338f5" # 二胺溶液配置工作流ID
workflow_id_2 = "3a19310d-16b9-9d81-b109-0748e953694b" # 90%10%小瓶投料工作流ID
#示例2创建二胺溶液配置任务 - ODA指定库位名称
# bioyond.create_diamine_solution_task(
# order_code="task_oda_" + str(int(datetime.now().timestamp())),
# order_name="二胺溶液配置-ODA",
# material_name="ODA-1",
# target_weigh="12.000",
# volume="60",
# liquid_material_name= "NMP",
# speed="400",
# temperature="20",
# delay_time="600",
# hold_m_name="烧杯ODA"
# )
# bioyond.create_diamine_solution_task(
# order_code="task_pda_" + str(int(datetime.now().timestamp())),
# order_name="二胺溶液配置-PDA",
# material_name="PDA-1",
# target_weigh="4.178",
# volume="60",
# liquid_material_name= "NMP",
# speed="400",
# temperature="20",
# delay_time="600",
# hold_m_name="烧杯PDA-2"
# )
# bioyond.create_diamine_solution_task(
# order_code="task_mpda_" + str(int(datetime.now().timestamp())),
# order_name="二胺溶液配置-MPDA",
# material_name="MPDA-1",
# target_weigh="3.298",
# volume="50",
# liquid_material_name= "NMP",
# speed="400",
# temperature="20",
# delay_time="600",
# hold_m_name="烧杯MPDA"
# )
bioyond.material_id_query("3a19310d-16b9-9d81-b109-0748e953694b")
bioyond.material_id_query("3a15d4a1-3bbe-76f9-a458-292896a338f5")
#示例4创建90%10%小瓶投料任务
# vial_result = bioyond.create_90_10_vial_feeding_task(
# order_code="task_vial_" + str(int(datetime.now().timestamp())),
# order_name="90%10%小瓶投料-1",
# percent_90_1_assign_material_name="BTDA-1",
# percent_90_1_target_weigh="7.392",
# percent_90_2_assign_material_name="BTDA-1",
# percent_90_2_target_weigh="7.392",
# percent_90_3_assign_material_name="BTDA-2",
# percent_90_3_target_weigh="7.392",
# percent_10_1_assign_material_name="BTDA-2",
# percent_10_1_target_weigh="1.500",
# percent_10_1_volume="20",
# percent_10_1_liquid_material_name="NMP",
# # percent_10_2_assign_material_name="BTDA-c",
# # percent_10_2_target_weigh="1.2",
# # percent_10_2_volume="20",
# # percent_10_2_liquid_material_name="NMP",
# speed="400",
# temperature="60",
# delay_time="1200",
# hold_m_name="8.4分装板-1"
# )
# vial_result = bioyond.create_90_10_vial_feeding_task(
# order_code="task_vial_" + str(int(datetime.now().timestamp())),
# order_name="90%10%小瓶投料-2",
# percent_90_1_assign_material_name="BPDA-1",
# percent_90_1_target_weigh="5.006",
# percent_90_2_assign_material_name="PMDA-1",
# percent_90_2_target_weigh="3.810",
# percent_90_3_assign_material_name="BPDA-1",
# percent_90_3_target_weigh="8.399",
# percent_10_1_assign_material_name="BPDA-1",
# percent_10_1_target_weigh="1.200",
# percent_10_1_volume="20",
# percent_10_1_liquid_material_name="NMP",
# percent_10_2_assign_material_name="BPDA-1",
# percent_10_2_target_weigh="1.200",
# percent_10_2_volume="20",
# percent_10_2_liquid_material_name="NMP",
# speed="400",
# temperature="60",
# delay_time="1200",
# hold_m_name="8.4分装板-2"
# )
#启动调度器
#bioyond.scheduler_start()
#继续调度器
#bioyond.scheduler_continue()
result0 = bioyond.stock_material('{"typeMode": 0, "includeDetail": true}')
result1 = bioyond.stock_material('{"typeMode": 1, "includeDetail": true}')
result2 = bioyond.stock_material('{"typeMode": 2, "includeDetail": true}')
matpos1 = bioyond.query_warehouse_by_material_type("3a14196e-b7a0-a5da-1931-35f3000281e9")
matpos2 = bioyond.query_warehouse_by_material_type("3a14196e-5dfe-6e21-0c79-fe2036d052c4")
matpos3 = bioyond.query_warehouse_by_material_type("3a14196b-24f2-ca49-9081-0cab8021bf1a")
#样品板(里面有样品瓶)
material_data_yp = {
"typeId": "3a14196e-b7a0-a5da-1931-35f3000281e9",
#"code": "物料编码001",
#"barCode": "物料条码001",
"name": "8.4样品板",
"unit": "",
"quantity": 1,
"details": [
{
"typeId": "3a14196a-cf7d-8aea-48d8-b9662c7dba94",
#"code": "物料编码001",
"name": "BTDA-1",
"quantity": 20,
"x": 1,
"y": 1,
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
},
{
"typeId": "3a14196a-cf7d-8aea-48d8-b9662c7dba94",
#"code": "物料编码001",
"name": "BPDA-1",
"quantity": 20,
"x": 2,
"y": 1, #x1y2是A02
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
},
{
"typeId": "3a14196a-cf7d-8aea-48d8-b9662c7dba94",
#"code": "物料编码001",
"name": "BTDA-2",
"quantity": 20,
"x": 1,
"y": 2, #x1y2是A02
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
},
{
"typeId": "3a14196a-cf7d-8aea-48d8-b9662c7dba94",
#"code": "物料编码001",
"name": "PMDA-1",
"quantity": 20,
"x": 2,
"y": 2, #x1y2是A02
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
}
],
"Parameters":"{}"
}
material_data_yp = {
"typeId": "3a14196e-b7a0-a5da-1931-35f3000281e9",
#"code": "物料编码001",
#"barCode": "物料条码001",
"name": "8.7样品板",
"unit": "",
"quantity": 1,
"details": [
{
"typeId": "3a14196a-cf7d-8aea-48d8-b9662c7dba94",
#"code": "物料编码001",
"name": "mianfen",
"quantity": 13,
"x": 1,
"y": 1,
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
},
{
"typeId": "3a14196a-cf7d-8aea-48d8-b9662c7dba94",
#"code": "物料编码001",
"name": "mianfen2",
"quantity": 13,
"x": 1,
"y": 2, #x1y2是A02
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
}
],
"Parameters":"{}"
}
#分装板
material_data_fzb_1 = {
"typeId": "3a14196e-5dfe-6e21-0c79-fe2036d052c4",
#"code": "物料编码001",
#"barCode": "物料条码001",
"name": "8.7分装板",
"unit": "",
"quantity": 1,
"details": [
{
"typeId": "3a14196c-76be-2279-4e22-7310d69aed68",
#"code": "物料编码001",
"name": "10%小瓶1",
"quantity": 1,
"x": 1,
"y": 1,
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
},
{
"typeId": "3a14196c-76be-2279-4e22-7310d69aed68",
#"code": "物料编码001",
"name": "10%小瓶2",
"quantity": 1,
"x": 1,
"y": 2,
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
},
{
"typeId": "3a14196c-76be-2279-4e22-7310d69aed68",
#"code": "物料编码001",
"name": "10%小瓶3",
"quantity": 1,
"x": 1,
"y": 3,
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
},
{
"typeId": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea",
#"code": "物料编码001",
"name": "90%小瓶1",
"quantity": 1,
"x": 2,
"y": 1, #x1y2是A02
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
},
{
"typeId": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea",
#"code": "物料编码001",
"name": "90%小瓶2",
"quantity": 1,
"x": 2,
"y": 2,
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
},
{
"typeId": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea",
#"code": "物料编码001",
"name": "90%小瓶3",
"quantity": 1,
"x": 2,
"y": 3,
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
}
],
"Parameters":"{}"
}
material_data_fzb_2 = {
"typeId": "3a14196e-5dfe-6e21-0c79-fe2036d052c4",
#"code": "物料编码001",
#"barCode": "物料条码001",
"name": "8.4分装板-2",
"unit": "",
"quantity": 1,
"details": [
{
"typeId": "3a14196c-76be-2279-4e22-7310d69aed68",
#"code": "物料编码001",
"name": "10%小瓶1",
"quantity": 1,
"x": 1,
"y": 1,
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
},
{
"typeId": "3a14196c-76be-2279-4e22-7310d69aed68",
#"code": "物料编码001",
"name": "10%小瓶2",
"quantity": 1,
"x": 1,
"y": 2,
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
},
{
"typeId": "3a14196c-76be-2279-4e22-7310d69aed68",
#"code": "物料编码001",
"name": "10%小瓶3",
"quantity": 1,
"x": 1,
"y": 3,
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
},
{
"typeId": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea",
#"code": "物料编码001",
"name": "90%小瓶1",
"quantity": 1,
"x": 2,
"y": 1, #x1y2是A02
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
},
{
"typeId": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea",
#"code": "物料编码001",
"name": "90%小瓶2",
"quantity": 1,
"x": 2,
"y": 2,
#"unit": "单位"
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
},
{
"typeId": "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea",
#"code": "物料编码001",
"name": "90%小瓶3",
"quantity": 1,
"x": 2,
"y": 3,
"molecular": 1,
"Parameters":"{\"molecular\": 1}"
}
],
"Parameters":"{}"
}
#烧杯
material_data_sb_oda = {
"typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
#"code": "物料编码001",
#"barCode": "物料条码001",
"name": "mianfen1",
"unit": "",
"quantity": 1,
"Parameters":"{}"
}
material_data_sb_pda_2 = {
"typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
#"code": "物料编码001",
#"barCode": "物料条码001",
"name": "mianfen2",
"unit": "",
"quantity": 1,
"Parameters":"{}"
}
# material_data_sb_mpda = {
# "typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a",
# #"code": "物料编码001",
# #"barCode": "物料条码001",
# "name": "烧杯MPDA",
# "unit": "个",
# "quantity": 1,
# "Parameters":"{}"
# }
#result_1 = bioyond.add_material(json.dumps(material_data_yp, ensure_ascii=False))
#result_2 = bioyond.add_material(json.dumps(material_data_fzb_1, ensure_ascii=False))
# result_3 = bioyond.add_material(json.dumps(material_data_fzb_2, ensure_ascii=False))
# result_4 = bioyond.add_material(json.dumps(material_data_sb_oda, ensure_ascii=False))
# result_5 = bioyond.add_material(json.dumps(material_data_sb_pda_2, ensure_ascii=False))
# #result会返回id
# #样品板1id3a1b3e7d-339d-0291-dfd3-13e2a78fe521
# #将指定物料入库到指定库位
#bioyond.material_inbound(result_1, "3a14198e-6929-31f0-8a22-0f98f72260df")
#bioyond.material_inbound(result_2, "3a14198e-6929-46fe-841e-03dd753f1e4a")
# bioyond.material_inbound(result_3, "3a14198e-6929-72ac-32ce-9b50245682b8")
# bioyond.material_inbound(result_4, "3a14198e-d724-e036-afdc-2ae39a7f3383")
# bioyond.material_inbound(result_5, "3a14198e-d724-d818-6d4f-5725191a24b5")
#bioyond.material_outbound(result_1, "3a14198e-6929-31f0-8a22-0f98f72260df")
# bioyond.stock_material('{"typeMode": 2, "includeDetail": true}')
query_order = {"status":"100", "pageCount": "10"}
bioyond.order_query(json.dumps(query_order, ensure_ascii=False))
# id = "3a1bce3c-4f31-c8f3-5525-f3b273bc34dc"
# bioyond.sample_waste_removal(id)

View File

@@ -1,11 +1,10 @@
# experiment_workflow.py
""" """
实验流程主程序 实验流程主程序
""" """
import json import json
from bioyond_rpc import BioyondV1RPC from unilabos.devices.workstation.bioyond_studio.reaction_station import BioyondReactionStation
from config import API_CONFIG, WORKFLOW_MAPPINGS from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG, WORKFLOW_MAPPINGS, DECK_CONFIG, MATERIAL_TYPE_MAPPINGS
def run_experiment(): def run_experiment():
@@ -14,15 +13,20 @@ 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
} }
Bioyond = BioyondV1RPC(config) # 创建BioyondReactionStation实例传入deck配置
Bioyond = BioyondReactionStation(
config=config,
deck=DECK_CONFIG
)
print("\n============= 多工作流参数测试(简化接口+材料缓存)=============") print("\n============= 多工作流参数测试(简化接口+材料缓存)=============")
# 显示可用的材料名称前20个 # 显示可用的材料名称前20个
available_materials = Bioyond.get_available_materials() available_materials = Bioyond.hardware_interface.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")
@@ -41,7 +45,7 @@ def run_experiment():
assign_material_name="ODA", assign_material_name="ODA",
time="0", time="0",
torque_variation="1", torque_variation="1",
titrationType="1", titration_type="1",
temperature=-10 temperature=-10
) )
@@ -52,14 +56,14 @@ def run_experiment():
assign_material_name="MPDA", assign_material_name="MPDA",
time="5", time="5",
torque_variation="2", torque_variation="2",
titrationType="1", titration_type="1",
temperature=0 temperature=0
) )
# 4. 液体投料-小瓶非滴定 # 4. 液体投料-小瓶非滴定
print("4. 添加液体投料-小瓶非滴定,带参数...") print("4. 添加液体投料-小瓶非滴定,带参数...")
Bioyond.liquid_feeding_vials_non_titration( Bioyond.liquid_feeding_vials_non_titration(
volumeFormula="639.5", volume_formula="639.5",
assign_material_name="SIDA", assign_material_name="SIDA",
titration_type="1", titration_type="1",
time="0", time="0",
@@ -84,7 +88,7 @@ def run_experiment():
material_id="3", material_id="3",
time="180", time="180",
torque_variation="2", torque_variation="2",
assign_material_name="BTDA-1", assign_material_name="BTDA1",
temperature=-10.00 temperature=-10.00
) )
@@ -93,7 +97,7 @@ def run_experiment():
material_id="3", material_id="3",
time="180", time="180",
torque_variation="2", torque_variation="2",
assign_material_name="BTDA-2", assign_material_name="BTDA2",
temperature=25.00 temperature=25.00
) )
@@ -102,14 +106,14 @@ def run_experiment():
material_id="3", material_id="3",
time="480", time="480",
torque_variation="2", torque_variation="2",
assign_material_name="BTDA-3", assign_material_name="BTDA3",
temperature=25.00 temperature=25.00
) )
# 液体投料滴定(第一个) # 液体投料滴定(第一个)
print("9. 添加液体投料滴定,带参数...") # ODPA print("9. 添加液体投料滴定,带参数...") # ODPA
Bioyond.liquid_feeding_titration( Bioyond.liquid_feeding_titration(
volume_formula="1000", volume_formula="{{6-0-5}}+{{7-0-5}}+{{8-0-5}}",
assign_material_name="BTDA-DD", assign_material_name="BTDA-DD",
titration_type="1", titration_type="1",
time="360", time="360",
@@ -169,8 +173,6 @@ def run_experiment():
temperature="25.00" temperature="25.00"
) )
print("15. 添加液体投料溶剂,带参数...") print("15. 添加液体投料溶剂,带参数...")
Bioyond.liquid_feeding_solvents( Bioyond.liquid_feeding_solvents(
assign_material_name="PGME", assign_material_name="PGME",
@@ -194,8 +196,8 @@ def run_experiment():
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_86", workflow_name="test3",
task_name="实验3_86" task_name="实验3"
) )
# 显示执行结果 # 显示执行结果
@@ -205,9 +207,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:
@@ -227,166 +229,166 @@ def run_experiment():
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__":

View File

@@ -0,0 +1,752 @@
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: 粉末类型ID
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=滴定, 其他=非滴定)
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,
titration_type: str = "1",
time: str = "360",
torque_variation: int = 2,
temperature: float = 25.00
):
"""液体进料-溶剂
Args:
assign_material_name: 物料名称
volume: 分液量(μL)
titration_type: 是否滴定
time: 观察时间(分钟)
torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是)
temperature: 温度上限(°C)
"""
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: 是否滴定
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: 是否滴定
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: 是否滴定
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
})
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

@@ -171,7 +171,6 @@ 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):
"""递归构建资源映射""" """递归构建资源映射"""

View File

@@ -668,7 +668,7 @@ __all__ = [
if __name__ == "__main__": if __name__ == "__main__":
# 简单测试HTTP服务 # 简单测试HTTP服务
class DummyWorkstation: class BioyondWorkstation:
device_id = "WS-001" device_id = "WS-001"
def process_step_finish_report(self, report_request): def process_step_finish_report(self, report_request):

View File

@@ -0,0 +1,252 @@
workstation.bioyond_dispensing_station:
category:
- workstation
- bioyond
class:
action_value_mappings:
create_90_10_vial_feeding_task:
feedback: {}
goal:
delay_time: delay_time
hold_m_name: hold_m_name
order_name: order_name
percent_10_1_assign_material_name: percent_10_1_assign_material_name
percent_10_1_liquid_material_name: percent_10_1_liquid_material_name
percent_10_1_target_weigh: percent_10_1_target_weigh
percent_10_1_volume: percent_10_1_volume
percent_10_2_assign_material_name: percent_10_2_assign_material_name
percent_10_2_liquid_material_name: percent_10_2_liquid_material_name
percent_10_2_target_weigh: percent_10_2_target_weigh
percent_10_2_volume: percent_10_2_volume
percent_10_3_assign_material_name: percent_10_3_assign_material_name
percent_10_3_liquid_material_name: percent_10_3_liquid_material_name
percent_10_3_target_weigh: percent_10_3_target_weigh
percent_10_3_volume: percent_10_3_volume
percent_90_1_assign_material_name: percent_90_1_assign_material_name
percent_90_1_target_weigh: percent_90_1_target_weigh
percent_90_2_assign_material_name: percent_90_2_assign_material_name
percent_90_2_target_weigh: percent_90_2_target_weigh
percent_90_3_assign_material_name: percent_90_3_assign_material_name
percent_90_3_target_weigh: percent_90_3_target_weigh
speed: speed
temperature: temperature
goal_default:
delay_time: ''
hold_m_name: ''
order_name: ''
percent_10_1_assign_material_name: ''
percent_10_1_liquid_material_name: ''
percent_10_1_target_weigh: ''
percent_10_1_volume: ''
percent_10_2_assign_material_name: ''
percent_10_2_liquid_material_name: ''
percent_10_2_target_weigh: ''
percent_10_2_volume: ''
percent_10_3_assign_material_name: ''
percent_10_3_liquid_material_name: ''
percent_10_3_target_weigh: ''
percent_10_3_volume: ''
percent_90_1_assign_material_name: ''
percent_90_1_target_weigh: ''
percent_90_2_assign_material_name: ''
percent_90_2_target_weigh: ''
percent_90_3_assign_material_name: ''
percent_90_3_target_weigh: ''
speed: ''
temperature: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: DispenStationVialFeed_Feedback
type: object
goal:
properties:
delay_time:
type: string
hold_m_name:
type: string
order_name:
type: string
percent_10_1_assign_material_name:
type: string
percent_10_1_liquid_material_name:
type: string
percent_10_1_target_weigh:
type: string
percent_10_1_volume:
type: string
percent_10_2_assign_material_name:
type: string
percent_10_2_liquid_material_name:
type: string
percent_10_2_target_weigh:
type: string
percent_10_2_volume:
type: string
percent_10_3_assign_material_name:
type: string
percent_10_3_liquid_material_name:
type: string
percent_10_3_target_weigh:
type: string
percent_10_3_volume:
type: string
percent_90_1_assign_material_name:
type: string
percent_90_1_target_weigh:
type: string
percent_90_2_assign_material_name:
type: string
percent_90_2_target_weigh:
type: string
percent_90_3_assign_material_name:
type: string
percent_90_3_target_weigh:
type: string
speed:
type: string
temperature:
type: string
required:
- order_name
- percent_90_1_assign_material_name
- percent_90_1_target_weigh
- percent_90_2_assign_material_name
- percent_90_2_target_weigh
- percent_90_3_assign_material_name
- percent_90_3_target_weigh
- percent_10_1_assign_material_name
- percent_10_1_target_weigh
- percent_10_1_volume
- percent_10_1_liquid_material_name
- percent_10_2_assign_material_name
- percent_10_2_target_weigh
- percent_10_2_volume
- percent_10_2_liquid_material_name
- percent_10_3_assign_material_name
- percent_10_3_target_weigh
- percent_10_3_volume
- percent_10_3_liquid_material_name
- speed
- temperature
- delay_time
- hold_m_name
title: DispenStationVialFeed_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: DispenStationVialFeed_Result
type: object
required:
- goal
title: DispenStationVialFeed
type: object
type: DispenStationVialFeed
create_diamine_solution_task:
feedback: {}
goal:
delay_time: delay_time
hold_m_name: hold_m_name
liquid_material_name: liquid_material_name
material_name: material_name
order_name: order_name
speed: speed
target_weigh: target_weigh
temperature: temperature
volume: volume
goal_default:
delay_time: ''
hold_m_name: ''
liquid_material_name: ''
material_name: ''
order_name: ''
speed: ''
target_weigh: ''
temperature: ''
volume: ''
handles: {}
result:
return_info: return_info
schema:
description: ''
properties:
feedback:
properties: {}
required: []
title: DispenStationSolnPrep_Feedback
type: object
goal:
properties:
delay_time:
type: string
hold_m_name:
type: string
liquid_material_name:
type: string
material_name:
type: string
order_name:
type: string
speed:
type: string
target_weigh:
type: string
temperature:
type: string
volume:
type: string
required:
- order_name
- material_name
- target_weigh
- volume
- liquid_material_name
- speed
- temperature
- delay_time
- hold_m_name
title: DispenStationSolnPrep_Goal
type: object
result:
properties:
return_info:
type: string
required:
- return_info
title: DispenStationSolnPrep_Result
type: object
required:
- goal
title: DispenStationSolnPrep
type: object
type: DispenStationSolnPrep
module: unilabos.devices.workstation.bioyond_studio.dispensing_station:BioyondDispensingStation
status_types: {}
type: python
config_info: []
description: ''
handles: []
icon: ''
init_param_schema:
config:
properties:
config:
type: string
required:
- config
type: object
data:
properties: {}
required: []
type: object
version: 1.0.0

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -4,11 +4,11 @@ reaction_station.bioyond:
- reaction_station_bioyond - reaction_station_bioyond
class: class:
action_value_mappings: action_value_mappings:
auto-add_material: auto-append_to_workflow_sequence:
feedback: {} feedback: {}
goal: {} goal: {}
goal_default: goal_default:
material_data: null web_workflow_name: null
handles: {} handles: {}
placeholder_keys: {} placeholder_keys: {}
result: {} result: {}
@@ -18,22 +18,21 @@ reaction_station.bioyond:
feedback: {} feedback: {}
goal: goal:
properties: properties:
material_data: web_workflow_name:
type: object type: string
required: required:
- material_data - web_workflow_name
type: object type: object
result: {} result: {}
required: required:
- goal - goal
title: add_material参数 title: append_to_workflow_sequence参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-create_90_10_vial_feeding_task: auto-clear_workflows:
feedback: {} feedback: {}
goal: {} goal: {}
goal_default: goal_default: {}
task_data: null
handles: {} handles: {}
placeholder_keys: {} placeholder_keys: {}
result: {} result: {}
@@ -42,470 +41,13 @@ reaction_station.bioyond:
properties: properties:
feedback: {} feedback: {}
goal: goal:
properties: properties: {}
task_data:
type: string
required:
- task_data
type: object
result: {}
required:
- goal
title: create_90_10_vial_feeding_task参数
type: object
type: UniLabJsonCommand
auto-create_batch_90_10_vial_feeding_task:
feedback: {}
goal: {}
goal_default:
batch_data: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
batch_data:
type: string
required:
- batch_data
type: object
result: {}
required:
- goal
title: create_batch_90_10_vial_feeding_task参数
type: object
type: UniLabJsonCommand
auto-create_batch_diamine_solution_task:
feedback: {}
goal: {}
goal_default:
batch_data: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
batch_data:
type: string
required:
- batch_data
type: object
result: {}
required:
- goal
title: create_batch_diamine_solution_task参数
type: object
type: UniLabJsonCommand
auto-create_diamine_solution_task:
feedback: {}
goal: {}
goal_default:
solution_data: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
solution_data:
type: string
required:
- solution_data
type: object
result: {}
required:
- goal
title: create_diamine_solution_task参数
type: object
type: UniLabJsonCommand
auto-create_order:
feedback: {}
goal: {}
goal_default:
parameters: null
task_name: null
workflow_name: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
parameters:
type: object
task_name:
type: string
workflow_name:
type: string
required:
- workflow_name
- task_name
type: object
result: {}
required:
- goal
title: create_order参数
type: object
type: UniLabJsonCommand
auto-create_resource:
feedback: {}
goal: {}
goal_default:
resource_data: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
resource_data:
type: string
required:
- resource_data
type: object
result: {}
required:
- goal
title: create_resource参数
type: object
type: UniLabJsonCommand
auto-delete_material:
feedback: {}
goal: {}
goal_default:
material_data: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_data:
type: string
required:
- material_data
type: object
result: {}
required:
- goal
title: delete_material参数
type: object
type: UniLabJsonCommand
auto-device_operation:
feedback: {}
goal: {}
goal_default:
device_id: null
operation: null
parameters: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
device_id:
type: string
operation:
type: string
parameters:
type: object
required:
- device_id
- operation
type: object
result: {}
required:
- goal
title: device_operation参数
type: object
type: UniLabJsonCommand
auto-dispensing_material_inbound:
feedback: {}
goal: {}
goal_default:
material_data: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_data:
type: string
required:
- material_data
type: object
result: {}
required:
- goal
title: dispensing_material_inbound参数
type: object
type: UniLabJsonCommand
auto-dispensing_material_outbound:
feedback: {}
goal: {}
goal_default:
material_data: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_data:
type: string
required:
- material_data
type: object
result: {}
required:
- goal
title: dispensing_material_outbound参数
type: object
type: UniLabJsonCommand
auto-drip_back:
feedback: {}
goal: {}
goal_default:
assign_material_name: Reactor
temperature: 25.0
time: '0'
torque_variation: '1'
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
assign_material_name:
default: Reactor
type: string
temperature:
default: 25.0
type: number
time:
default: '0'
type: string
torque_variation:
default: '1'
type: string
required: [] required: []
type: object type: object
result: {} result: {}
required: required:
- goal - goal
title: drip_back参数 title: clear_workflows参数
type: object
type: UniLabJsonCommand
auto-execute_bioyond_sync_workflow:
feedback: {}
goal: {}
goal_default:
parameters: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
parameters:
type: object
required:
- parameters
type: object
result: {}
required:
- goal
title: execute_bioyond_sync_workflow参数
type: object
type: UniLabJsonCommandAsync
auto-execute_bioyond_update_workflow:
feedback: {}
goal: {}
goal_default:
parameters: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
parameters:
type: object
required:
- parameters
type: object
result: {}
required:
- goal
title: execute_bioyond_update_workflow参数
type: object
type: UniLabJsonCommandAsync
auto-liquid_feeding_beaker:
feedback: {}
goal: {}
goal_default:
material_name: ''
volume: ''
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_name:
default: ''
type: string
volume:
default: ''
type: string
required: []
type: object
result: {}
required:
- goal
title: liquid_feeding_beaker参数
type: object
type: UniLabJsonCommand
auto-liquid_feeding_solvents:
feedback: {}
goal: {}
goal_default:
material_name: ''
volume: ''
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_name:
default: ''
type: string
volume:
default: ''
type: string
required: []
type: object
result: {}
required:
- goal
title: liquid_feeding_solvents参数
type: object
type: UniLabJsonCommand
auto-liquid_feeding_titration:
feedback: {}
goal: {}
goal_default:
material_name: ''
time: '120'
titration_type: '1'
torque_variation: '2'
volume: ''
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_name:
default: ''
type: string
time:
default: '120'
type: string
titration_type:
default: '1'
type: string
torque_variation:
default: '2'
type: string
volume:
default: ''
type: string
required: []
type: object
result: {}
required:
- goal
title: liquid_feeding_titration参数
type: object
type: UniLabJsonCommand
auto-liquid_feeding_vials_non_titration:
feedback: {}
goal: {}
goal_default:
material_name: ''
volume: ''
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_name:
default: ''
type: string
volume:
default: ''
type: string
required: []
type: object
result: {}
required:
- goal
title: liquid_feeding_vials_non_titration参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-load_bioyond_data_from_file: auto-load_bioyond_data_from_file:
@@ -533,74 +75,11 @@ reaction_station.bioyond:
title: load_bioyond_data_from_file参数 title: load_bioyond_data_from_file参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-material_inbound:
feedback: {}
goal: {}
goal_default:
location_name: null
material_id: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
location_name:
type: string
material_id:
type: string
required:
- material_id
- location_name
type: object
result: {}
required:
- goal
title: material_inbound参数
type: object
type: UniLabJsonCommand
auto-material_outbound:
feedback: {}
goal: {}
goal_default:
location_name: null
material_id: null
quantity: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
location_name:
type: string
material_id:
type: string
quantity:
type: integer
required:
- material_id
- location_name
- quantity
type: object
result: {}
required:
- goal
title: material_outbound参数
type: object
type: UniLabJsonCommand
auto-merge_workflow_with_parameters: auto-merge_workflow_with_parameters:
feedback: {} feedback: {}
goal: {} goal: {}
goal_default: goal_default:
name: null json_str: null
workflows: null
handles: {} handles: {}
placeholder_keys: {} placeholder_keys: {}
result: {} result: {}
@@ -610,15 +89,10 @@ reaction_station.bioyond:
feedback: {} feedback: {}
goal: goal:
properties: properties:
name: json_str:
type: string type: string
workflows:
items:
type: object
type: array
required: required:
- name - json_str
- workflows
type: object type: object
result: {} result: {}
required: required:
@@ -626,31 +100,6 @@ reaction_station.bioyond:
title: merge_workflow_with_parameters参数 title: merge_workflow_with_parameters参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-order_query:
feedback: {}
goal: {}
goal_default:
query_data: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
query_data:
type: string
required:
- query_data
type: object
result: {}
required:
- goal
title: order_query参数
type: object
type: UniLabJsonCommand
auto-post_init: auto-post_init:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -676,33 +125,11 @@ reaction_station.bioyond:
title: post_init参数 title: post_init参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-reactor_taken_in: auto-process_web_workflows:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: reactor_taken_in参数
type: object
type: UniLabJsonCommand
auto-reactor_taken_out:
feedback: {} feedback: {}
goal: {} goal: {}
goal_default: goal_default:
order_id: '' json_str: null
preintake_id: ''
handles: {} handles: {}
placeholder_keys: {} placeholder_keys: {}
result: {} result: {}
@@ -712,18 +139,15 @@ reaction_station.bioyond:
feedback: {} feedback: {}
goal: goal:
properties: properties:
order_id: json_str:
default: ''
type: string type: string
preintake_id: required:
default: '' - json_str
type: string
required: []
type: object type: object
result: {} result: {}
required: required:
- goal - goal
title: reactor_taken_out参数 title: process_web_workflows参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-reset_workstation: auto-reset_workstation:
@@ -747,11 +171,11 @@ reaction_station.bioyond:
title: reset_workstation参数 title: reset_workstation参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-sample_waste_removal: auto-resource_tree_add:
feedback: {} feedback: {}
goal: {} goal: {}
goal_default: goal_default:
waste_data: null resources: null
handles: {} handles: {}
placeholder_keys: {} placeholder_keys: {}
result: {} result: {}
@@ -761,119 +185,42 @@ reaction_station.bioyond:
feedback: {} feedback: {}
goal: goal:
properties: properties:
waste_data: resources:
items:
type: object
type: array
required:
- resources
type: object
result: {}
required:
- goal
title: resource_tree_add参数
type: object
type: UniLabJsonCommand
auto-set_workflow_sequence:
feedback: {}
goal: {}
goal_default:
json_str: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
json_str:
type: string type: string
required: required:
- waste_data - json_str
type: object type: object
result: {} result: {}
required: required:
- goal - goal
title: sample_waste_removal参数 title: set_workflow_sequence参数
type: object
type: UniLabJsonCommand
auto-solid_feeding_vials:
feedback: {}
goal: {}
goal_default:
material_name: ''
volume: ''
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
material_name:
default: ''
type: string
volume:
default: ''
type: string
required: []
type: object
result: {}
required:
- goal
title: solid_feeding_vials参数
type: object
type: UniLabJsonCommand
auto-start_scheduler:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: start_scheduler参数
type: object
type: UniLabJsonCommand
auto-stock_material:
feedback: {}
goal: {}
goal_default:
location: null
material_id: null
quantity: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
location:
type: string
material_id:
type: string
quantity:
type: integer
required:
- material_id
- location
- quantity
type: object
result: {}
required:
- goal
title: stock_material参数
type: object
type: UniLabJsonCommand
auto-stop_scheduler:
feedback: {}
goal: {}
goal_default: {}
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties: {}
required: []
type: object
result: {}
required:
- goal
title: stop_scheduler参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-transfer_resource_to_another: auto-transfer_resource_to_another:
@@ -1064,33 +411,6 @@ reaction_station.bioyond:
title: transfer_resource_to_another参数 title: transfer_resource_to_another参数
type: object type: object
type: UniLabJsonCommand type: UniLabJsonCommand
auto-validate_workflow_parameters:
feedback: {}
goal: {}
goal_default:
workflows: null
handles: {}
placeholder_keys: {}
result: {}
schema:
description: ''
properties:
feedback: {}
goal:
properties:
workflows:
items:
type: object
type: array
required:
- workflows
type: object
result: {}
required:
- goal
title: validate_workflow_parameters参数
type: object
type: UniLabJsonCommand
bioyond_sync: bioyond_sync:
feedback: {} feedback: {}
goal: goal:
@@ -1407,11 +727,8 @@ reaction_station.bioyond:
module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation
protocol_type: [] protocol_type: []
status_types: status_types:
all_workflows: dict
bioyond_status: dict bioyond_status: dict
device_list: dict
scheduler_status: dict
station_info: dict
workflow_parameter_template: dict
workstation_status: dict workstation_status: dict
type: python type: python
config_info: [] config_info: []
@@ -1431,24 +748,15 @@ reaction_station.bioyond:
type: object type: object
data: data:
properties: properties:
all_workflows:
type: object
bioyond_status: bioyond_status:
type: object type: object
device_list:
type: object
scheduler_status:
type: object
station_info:
type: object
workflow_parameter_template:
type: object
workstation_status: workstation_status:
type: object type: object
required: required:
- station_info
- bioyond_status - bioyond_status
- workflow_parameter_template - all_workflows
- scheduler_status
- device_list
- workstation_status - workstation_status
type: object type: object
version: 1.0.0 version: 1.0.0

File diff suppressed because it is too large Load Diff

View File

@@ -708,6 +708,8 @@ 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

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

View File

@@ -5,15 +5,15 @@ def bioyond_warehouse_1x4x4(name: str) -> WareHouse:
"""创建BioYond 4x1x4仓库""" """创建BioYond 4x1x4仓库"""
return warehouse_factory( return warehouse_factory(
name=name, name=name,
num_items_x=1, num_items_x=4,
num_items_y=4, num_items_y=4,
num_items_z=4, num_items_z=1,
dx=10.0, dx=10.0,
dy=10.0, dy=10.0,
dz=10.0, dz=10.0,
item_dx=137.0, item_dx=147.0,
item_dy=96.0, item_dy=106.0,
item_dz=120.0, item_dz=130.0,
category="warehouse", category="warehouse",
) )

View File

@@ -1,67 +1,84 @@
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(object): class RegularContainer(Container):
# 第一个参数必须是id传入 def __init__(self, *args, **kwargs):
# noinspection PyShadowingBuiltins if "size_x" not in kwargs:
def __init__(self, id: str): kwargs["size_x"] = 0
self.id = id if "size_y" not in kwargs:
self.ulr_resource = Resource() kwargs["size_y"] = 0
self._data = None if "size_z" not in kwargs:
kwargs["size_z"] = 0
self.kwargs = kwargs
self.state = {}
super().__init__(*args, **kwargs)
@property def load_state(self, state: Dict[str, Any]):
def ulr_resource_data(self): self.state = state
if self._data is None: #
self._data = json.loads(self.ulr_resource.data) if self.ulr_resource.data else {} # class RegularContainer(object):
return self._data # # 第一个参数必须是id传入
# # noinspection PyShadowingBuiltins
@ulr_resource_data.setter # def __init__(self, id: str):
def ulr_resource_data(self, value: dict): # self.id = id
self._data = value # self.ulr_resource = Resource()
self.ulr_resource.data = json.dumps(self._data) # self._data = None
#
@property # @property
def liquid_type(self): # def ulr_resource_data(self):
return self.ulr_resource_data.get("liquid_type", None) # if self._data is None:
# self._data = json.loads(self.ulr_resource.data) if self.ulr_resource.data else {}
@liquid_type.setter # return self._data
def liquid_type(self, value: str): #
if value is not None: # @ulr_resource_data.setter
self.ulr_resource_data["liquid_type"] = value # def ulr_resource_data(self, value: dict):
else: # self._data = value
self.ulr_resource_data.pop("liquid_type", None) # self.ulr_resource.data = json.dumps(self._data)
#
@property # @property
def liquid_volume(self): # def liquid_type(self):
return self.ulr_resource_data.get("liquid_volume", None) # return self.ulr_resource_data.get("liquid_type", None)
#
@liquid_volume.setter # @liquid_type.setter
def liquid_volume(self, value: float): # def liquid_type(self, value: str):
if value is not None: # if value is not None:
self.ulr_resource_data["liquid_volume"] = value # self.ulr_resource_data["liquid_type"] = value
else: # else:
self.ulr_resource_data.pop("liquid_volume", None) # self.ulr_resource_data.pop("liquid_type", None)
#
def get_ulr_resource(self) -> Resource: # @property
""" # def liquid_volume(self):
获取UlrResource对象 # return self.ulr_resource_data.get("liquid_volume", None)
:return: UlrResource对象 #
""" # @liquid_volume.setter
self.ulr_resource_data = self.ulr_resource_data # 确保数据被更新 # def liquid_volume(self, value: float):
return self.ulr_resource # if value is not None:
# self.ulr_resource_data["liquid_volume"] = value
def get_ulr_resource_as_dict(self) -> Resource: # else:
""" # self.ulr_resource_data.pop("liquid_volume", None)
获取UlrResource对象 #
:return: UlrResource对象 # def get_ulr_resource(self) -> Resource:
""" # """
to_dict = convert_from_ros_msg(self.get_ulr_resource()) # 获取UlrResource对象
to_dict["type"] = "container" # :return: UlrResource对象
return to_dict # """
# self.ulr_resource_data = self.ulr_resource_data # 确保数据被更新
def __str__(self): # return self.ulr_resource
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,18 +1,23 @@
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 from typing import Union, Any, Dict, List, Tuple
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:
@@ -44,6 +49,33 @@ 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)}
@@ -301,6 +333,10 @@ 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)
@@ -576,13 +612,13 @@ 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 = {}, deck: Any = None) -> list[dict]: def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: Dict[str, Tuple[str, str]] = {}, deck: Any = None) -> list[dict]:
""" """
将 bioyond 物料格式转换为 ulab 物料格式 将 bioyond 物料格式转换为 ulab 物料格式
Args: Args:
bioyond_materials: bioyond 系统的物料查询结果列表 bioyond_materials: bioyond 系统的物料查询结果列表
type_mapping: 物料类型映射字典,格式 {bioyond_type: plr_class_name} type_mapping: 物料类型映射字典,格式 {bioyond_type: [plr_class_name, class_uuid]}
location_id_mapping: 库位 ID 到名称的映射字典,格式 {location_id: location_name} location_id_mapping: 库位 ID 到名称的映射字典,格式 {location_id: location_name}
Returns: Returns:
@@ -592,35 +628,35 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict =
for material in bioyond_materials: for material in bioyond_materials:
className = ( className = (
type_mapping.get(material.get("typeName"), "RegularContainer") if type_mapping else "RegularContainer" type_mapping.get(material.get("typeName"), ("RegularContainer", ""))[0] 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
) )
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("x", 0) - 1) * plr_material.num_items_x + (detail.get("y", 0) - 1) * plr_material.num_items_y
+ (detail.get("y", 0) - 1) + (detail.get("x", 0) - 1)
) )
bottle = plr_material[number] typeName = detail.get("typeName", detail.get("name", ""))
if detail["name"] in type_mapping: if typeName in type_mapping:
# plr_material.unassign_child_resource(bottle) bottle = plr_material[number] = initialize_resource(
plr_material.sites[number] = None {"name": f'{detail["name"]}_{number}', "class": type_mapping[typeName][0]}, resource_type=ResourcePLR
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 = [
@@ -645,32 +681,59 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict =
return plr_materials return plr_materials
def resource_plr_to_bioyond(plr_materials: list[ResourcePLR], type_mapping: dict = {}, warehouse_mapping: dict = {}) -> list[dict]: def resource_plr_to_bioyond(plr_resources: list[ResourcePLR], type_mapping: dict = {}, warehouse_mapping: dict = {}) -> list[dict]:
bioyond_materials = [] bioyond_materials = []
for plr_material in plr_materials: for resource in plr_resources:
material = { if hasattr(resource, "capacity") and resource.capacity > 1:
"name": plr_material.name, material = {
"typeName": plr_material.__class__.__name__, "typeId": type_mapping.get(resource.model)[1],
"code": plr_material.code, "name": resource.name,
"quantity": 0, "unit": "",
"detail": [], "quantity": 1,
"locations": [], "details": [],
} "Parameters": "{}"
if hasattr(plr_material, "capacity") and plr_material.capacity > 1: }
for idx in range(plr_material.capacity): for bottle in resource.children:
bottle = plr_material[idx] if isinstance(resource, ItemizedCarrier):
detail = { site = resource.get_child_identifier(bottle)
"x": (idx // (plr_material.num_items_x * plr_material.num_items_y)) + 1, else:
"y": ((idx % (plr_material.num_items_x * plr_material.num_items_y)) // plr_material.num_items_x) + 1, site = {"x": bottle.location.x - 1, "y": bottle.location.y - 1}
"z": (idx % plr_material.num_items_x) + 1, detail_item = {
"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["detail"].append(detail) material["details"].append(detail_item)
material["quantity"] = 1.0
else: else:
bottle = plr_material[0] if plr_material.capacity > 0 else plr_material bottle = resource[0] if resource.capacity > 0 else resource
material["quantity"] = sum(qty for _, qty in bottle.tracker.liquids) if hasattr(plr_material, "tracker") else 0 material = {
"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
@@ -717,6 +780,7 @@ 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,6 +32,7 @@ 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,
@@ -73,6 +74,7 @@ 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,
@@ -87,6 +89,8 @@ 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.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())
@@ -149,7 +153,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(str(spot)), spot=spot) self.assign_child_resource(resource, location=self.child_locations.get(list(self._ordering.keys())[spot]), spot=spot)
def unassign_child_resource(self, resource: ResourcePLR): def unassign_child_resource(self, resource: ResourcePLR):
found = False found = False
@@ -160,8 +164,92 @@ 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")
if hasattr(resource, "unassign"): super().unassign_child_resource(resource)
resource.unassign() # if hasattr(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,
@@ -319,6 +407,7 @@ 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": True if self[identifier] is not None else False, "visible": True if self[identifier] is not None else False,
@@ -344,6 +433,7 @@ 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,
**kwargs,
): ):
super().__init__( super().__init__(
name=name, name=name,

View File

@@ -5,10 +5,13 @@ 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 warehouse_factory(
name: str, name: str,
num_items_x: int = 4, num_items_x: int = 1,
num_items_y: int = 1, num_items_y: int = 4,
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,
@@ -33,13 +36,16 @@ def 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[j]}{i + 1}" for i in range(len_x) for j in range(len_y)]
sites = {i: site for i, site in zip(keys, _sites.values())}
return WareHouse( return WareHouse(
name=name, name=name,
@@ -68,6 +74,7 @@ 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,
@@ -83,6 +90,7 @@ 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,6 +26,7 @@ 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}")
@@ -50,7 +51,7 @@ def initialize_device_from_dict(device_id, device_config) -> Optional[ROS2Device
) )
try: try:
d = DEVICE( d = DEVICE(
device_id=device_id, driver_is_ros=device_class_config["type"] == "ros2", driver_params=device_config.get("config", {}) device_id=device_id, device_uuid=uid, 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

View File

@@ -6,7 +6,7 @@ import threading
import time import time
import traceback import traceback
import uuid import uuid
from typing import get_type_hints, TypeVar, Generic, Dict, Any, Type, TypedDict, Optional, List, TYPE_CHECKING from typing import get_type_hints, TypeVar, Generic, Dict, Any, Type, TypedDict, Optional, List, TYPE_CHECKING, Union
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
import asyncio import asyncio
@@ -132,6 +132,7 @@ class ROSLoggerAdapter:
def init_wrapper( def init_wrapper(
self, self,
device_id: str, device_id: str,
device_uuid: str,
driver_class: type[T], driver_class: type[T],
device_config: Dict[str, Any], device_config: Dict[str, Any],
status_types: Dict[str, Any], status_types: Dict[str, Any],
@@ -150,6 +151,7 @@ def init_wrapper(
if children is None: if children is None:
children = [] children = []
kwargs["device_id"] = device_id kwargs["device_id"] = device_id
kwargs["device_uuid"] = device_uuid
kwargs["driver_class"] = driver_class kwargs["driver_class"] = driver_class
kwargs["device_config"] = device_config kwargs["device_config"] = device_config
kwargs["driver_params"] = driver_params kwargs["driver_params"] = driver_params
@@ -266,6 +268,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
self, self,
driver_instance: T, driver_instance: T,
device_id: str, device_id: str,
device_uuid: str,
status_types: Dict[str, Any], status_types: Dict[str, Any],
action_value_mappings: Dict[str, Any], action_value_mappings: Dict[str, Any],
hardware_interface: Dict[str, Any], hardware_interface: Dict[str, Any],
@@ -278,6 +281,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
Args: Args:
driver_instance: 设备实例 driver_instance: 设备实例
device_id: 设备标识符 device_id: 设备标识符
device_uuid: 设备标识符
status_types: 需要发布的状态和传感器信息 status_types: 需要发布的状态和传感器信息
action_value_mappings: 设备动作 action_value_mappings: 设备动作
hardware_interface: 硬件接口配置 hardware_interface: 硬件接口配置
@@ -285,7 +289,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
""" """
self.driver_instance = driver_instance self.driver_instance = driver_instance
self.device_id = device_id self.device_id = device_id
self.uuid = str(uuid.uuid4()) self.uuid = device_uuid
self.publish_high_frequency = False self.publish_high_frequency = False
self.callback_group = ReentrantCallbackGroup() self.callback_group = ReentrantCallbackGroup()
self.resource_tracker = resource_tracker self.resource_tracker = resource_tracker
@@ -554,6 +558,11 @@ class BaseROS2DeviceNode(Node, Generic[T]):
async def update_resource(self, resources: List["ResourcePLR"]): async def update_resource(self, resources: List["ResourcePLR"]):
r = SerialCommand.Request() r = SerialCommand.Request()
tree_set = ResourceTreeSet.from_plr_resources(resources) tree_set = ResourceTreeSet.from_plr_resources(resources)
for tree in tree_set.trees:
root_node = tree.root_node
if not root_node.res_content.uuid_parent:
logger.warning(f"更新无父节点物料{root_node},自动以当前设备作为根节点")
root_node.res_content.parent_uuid = self.uuid
r.command = json.dumps({"data": {"data": tree_set.dump()}, "action": "update"}) r.command = json.dumps({"data": {"data": tree_set.dump()}, "action": "update"})
response: SerialCommand_Response = await self._resource_clients["c2s_update_resource_tree"].call_async(r) # type: ignore response: SerialCommand_Response = await self._resource_clients["c2s_update_resource_tree"].call_async(r) # type: ignore
try: try:
@@ -573,6 +582,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
- update: 更新现有资源 - update: 更新现有资源
- remove: 从资源树中移除资源 - remove: 从资源树中移除资源
""" """
from pylabrobot.resources.resource import Resource as ResourcePLR
try: try:
data = json.loads(req.command) data = json.loads(req.command)
results = [] results = []
@@ -648,15 +658,28 @@ class BaseROS2DeviceNode(Node, Generic[T]):
results.append({"success": True, "action": "update"}) results.append({"success": True, "action": "update"})
elif action == "remove": elif action == "remove":
# 移除资源 # 移除资源
plr_resources: List[ResourcePLR] = [ found_resources: List[List[Union[ResourcePLR, dict]]] = self.resource_tracker.figure_resource(
self.resource_tracker.uuid_to_resources[i] for i in resources_uuid [{"uuid": uid} for uid in resources_uuid], try_mode=True
] )
found_plr_resources = []
other_plr_resources = []
for found_resource in found_resources:
for resource in found_resource:
if issubclass(resource.__class__, ResourcePLR):
found_plr_resources.append(resource)
else:
other_plr_resources.append(resource)
func = getattr(self.driver_instance, "resource_tree_remove", None) func = getattr(self.driver_instance, "resource_tree_remove", None)
if callable(func): if callable(func):
func(plr_resources) func(found_plr_resources)
for plr_resource in plr_resources: for plr_resource in found_plr_resources:
plr_resource.parent.unassign_child_resource(plr_resource) if plr_resource.parent is not None:
plr_resource.parent.unassign_child_resource(plr_resource)
self.resource_tracker.remove_resource(plr_resource) self.resource_tracker.remove_resource(plr_resource)
self.lab_logger().info(f"移除物料 {plr_resource} 及其子节点")
for other_plr_resource in other_plr_resources:
self.resource_tracker.remove_resource(other_plr_resource)
self.lab_logger().info(f"移除物料 {other_plr_resource} 及其子节点")
results.append({"success": True, "action": "remove"}) results.append({"success": True, "action": "remove"})
except Exception as e: except Exception as e:
error_msg = f"Error processing {action} operation: {str(e)}" error_msg = f"Error processing {action} operation: {str(e)}"
@@ -920,7 +943,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
queried_resources = [] queried_resources = []
for resource_data in resource_inputs: for resource_data in resource_inputs:
r = SerialCommand.Request() r = SerialCommand.Request()
r.command = json.dumps({"id": resource_data["id"], "with_children": True}) r.command = json.dumps({"id": resource_data["id"], "uuid": resource_data.get("uuid", None), "with_children": True})
# 发送请求并等待响应 # 发送请求并等待响应
response: SerialCommand_Response = await self._resource_clients[ response: SerialCommand_Response = await self._resource_clients[
"resource_get" "resource_get"
@@ -936,7 +959,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
# 通过资源跟踪器获取本地实例 # 通过资源跟踪器获取本地实例
final_resources = queried_resources if is_sequence else queried_resources[0] final_resources = queried_resources if is_sequence else queried_resources[0]
action_kwargs[k] = self.resource_tracker.figure_resource(final_resources, try_mode=False) final_resources = self.resource_tracker.figure_resource({"name": final_resources.name}, try_mode=False) if not is_sequence else [
self.resource_tracker.figure_resource({"name": res.name}, try_mode=False) for res in queried_resources
]
action_kwargs[k] = final_resources
except Exception as e: except Exception as e:
self.lab_logger().error(f"{action_name} 物料实例获取失败: {e}\n{traceback.format_exc()}") self.lab_logger().error(f"{action_name} 物料实例获取失败: {e}\n{traceback.format_exc()}")
@@ -1347,6 +1373,7 @@ class ROS2DeviceNode:
def __init__( def __init__(
self, self,
device_id: str, device_id: str,
device_uuid: str,
driver_class: Type[T], driver_class: Type[T],
device_config: Dict[str, Any], device_config: Dict[str, Any],
driver_params: Dict[str, Any], driver_params: Dict[str, Any],
@@ -1362,6 +1389,7 @@ class ROS2DeviceNode:
Args: Args:
device_id: 设备标识符 device_id: 设备标识符
device_uuid: 设备uuid
driver_class: 设备类 driver_class: 设备类
device_config: 原始初始化的json device_config: 原始初始化的json
driver_params: driver初始化的参数 driver_params: driver初始化的参数
@@ -1436,6 +1464,7 @@ class ROS2DeviceNode:
children=children, children=children,
driver_instance=self._driver_instance, # type: ignore driver_instance=self._driver_instance, # type: ignore
device_id=device_id, device_id=device_id,
device_uuid=device_uuid,
status_types=status_types, status_types=status_types,
action_value_mappings=action_value_mappings, action_value_mappings=action_value_mappings,
hardware_interface=hardware_interface, hardware_interface=hardware_interface,
@@ -1446,6 +1475,7 @@ class ROS2DeviceNode:
self._ros_node = BaseROS2DeviceNode( self._ros_node = BaseROS2DeviceNode(
driver_instance=self._driver_instance, driver_instance=self._driver_instance,
device_id=device_id, device_id=device_id,
device_uuid=device_uuid,
status_types=status_types, status_types=status_types,
action_value_mappings=action_value_mappings, action_value_mappings=action_value_mappings,
hardware_interface=hardware_interface, hardware_interface=hardware_interface,

View File

@@ -18,7 +18,7 @@ from unilabos_msgs.srv import (
ResourceDelete, ResourceDelete,
ResourceUpdate, ResourceUpdate,
ResourceList, ResourceList,
SerialCommand, SerialCommand, ResourceGet,
) # type: ignore ) # type: ignore
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
from unique_identifier_msgs.msg import UUID from unique_identifier_msgs.msg import UUID
@@ -41,6 +41,7 @@ from unilabos.ros.nodes.resource_tracker import (
ResourceTreeSet, ResourceTreeSet,
ResourceTreeInstance, ResourceTreeInstance,
) )
from unilabos.utils import logger
from unilabos.utils.exception import DeviceClassInvalid from unilabos.utils.exception import DeviceClassInvalid
from unilabos.utils.type_check import serialize_result_info from unilabos.utils.type_check import serialize_result_info
from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot from unilabos.registry.placeholder_type import ResourceSlot, DeviceSlot
@@ -99,17 +100,6 @@ class HostNode(BaseROS2DeviceNode):
""" """
if self._instance is not None: if self._instance is not None:
self._instance.lab_logger().critical("[Host Node] HostNode instance already exists.") self._instance.lab_logger().critical("[Host Node] HostNode instance already exists.")
# 初始化Node基类传递空参数覆盖列表
BaseROS2DeviceNode.__init__(
self,
driver_instance=self,
device_id=device_id,
status_types={},
action_value_mappings=lab_registry.device_type_registry["host_node"]["class"]["action_value_mappings"],
hardware_interface={},
print_publish=False,
resource_tracker=self._resource_tracker, # host node并不是通过initialize 包一层传进来的
)
# 设置单例实例 # 设置单例实例
self.__class__._instance = self self.__class__._instance = self
@@ -127,6 +117,91 @@ class HostNode(BaseROS2DeviceNode):
bridges = [] bridges = []
self.bridges = bridges self.bridges = bridges
# 创建 host_node 作为一个单独的 ResourceTree
host_node_dict = {
"id": "host_node",
"uuid": str(uuid.uuid4()),
"parent_uuid": "",
"name": "host_node",
"type": "device",
"class": "host_node",
"config": {},
"data": {},
"children": [],
"description": "",
"schema": {},
"model": {},
"icon": "",
}
# 创建 host_node 的 ResourceTree
host_node_instance = ResourceDictInstance.get_resource_instance_from_dict(host_node_dict)
host_node_tree = ResourceTreeInstance(host_node_instance)
resources_config.trees.insert(0, host_node_tree)
try:
for bridge in self.bridges:
if hasattr(bridge, "resource_tree_add") and resources_config:
from unilabos.app.web.client import HTTPClient
client: HTTPClient = bridge
resource_start_time = time.time()
# 传递 ResourceTreeSet 对象,在 client 中转换为字典并获取 UUID 映射
uuid_mapping = client.resource_tree_add(resources_config, "", True)
device_uuid = resources_config.root_nodes[0].res_content.uuid
resource_end_time = time.time()
logger.info(
f"[Host Node-Resource] 物料上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms"
)
for edge in self.resources_edge_config:
edge["source_uuid"] = uuid_mapping.get(edge["source_uuid"], edge["source_uuid"])
edge["target_uuid"] = uuid_mapping.get(edge["target_uuid"], edge["target_uuid"])
resource_add_res = client.resource_edge_add(self.resources_edge_config)
resource_edge_end_time = time.time()
logger.info(
f"[Host Node-Resource] 物料关系上传 {round(resource_edge_end_time - resource_end_time, 5) * 1000} ms"
)
# resources_config 通过各个设备的 resource_tracker 进行uuid更新利用uuid_mapping
# resources_config 的 root node 是
# # 创建反向映射new_uuid -> old_uuid
# reverse_uuid_mapping = {new_uuid: old_uuid for old_uuid, new_uuid in uuid_mapping.items()}
# for tree in resources_config.trees:
# node = tree.root_node
# if node.res_content.type == "device":
# if node.res_content.id == "host_node":
# continue
# # slave节点走c2s更新接口拿到add自行update uuid
# device_tracker = self.devices_instances[node.res_content.id].resource_tracker
# old_uuid = reverse_uuid_mapping.get(node.res_content.uuid)
# if old_uuid:
# # 找到旧UUID使用UUID查找
# resource_instance = device_tracker.uuid_to_resources.get(old_uuid)
# else:
# # 未找到旧UUID使用name查找
# resource_instance = device_tracker.figure_resource(
# {"name": node.res_content.name}
# )
# device_tracker.loop_update_uuid(resource_instance, uuid_mapping)
# else:
# try:
# for plr_resource in ResourceTreeSet([tree]).to_plr_resources():
# self.resource_tracker.add_resource(plr_resource)
# except Exception as ex:
# self.lab_logger().warning("[Host Node-Resource] 根节点物料序列化失败!")
except Exception as ex:
logger.error(f"[Host Node-Resource] 添加物料出错!\n{traceback.format_exc()}")
# 初始化Node基类传递空参数覆盖列表
BaseROS2DeviceNode.__init__(
self,
driver_instance=self,
device_id=device_id,
device_uuid=host_node_dict["uuid"],
status_types={},
action_value_mappings=lab_registry.device_type_registry["host_node"]["class"]["action_value_mappings"],
hardware_interface={},
print_publish=False,
resource_tracker=self._resource_tracker, # host node并不是通过initialize 包一层传进来的
)
# 创建设备、动作客户端和目标存储 # 创建设备、动作客户端和目标存储
self.devices_names: Dict[str, str] = {device_id: self.namespace} # 存储设备名称和命名空间的映射 self.devices_names: Dict[str, str] = {device_id: self.namespace} # 存储设备名称和命名空间的映射
self.devices_instances: Dict[str, ROS2DeviceNode] = {} # 存储设备实例 self.devices_instances: Dict[str, ROS2DeviceNode] = {} # 存储设备实例
@@ -207,81 +282,7 @@ class HostNode(BaseROS2DeviceNode):
].items(): ].items():
controller_config["update_rate"] = update_rate controller_config["update_rate"] = update_rate
self.initialize_controller(controller_id, controller_config) self.initialize_controller(controller_id, controller_config)
# 创建 host_node 作为一个单独的 ResourceTree
host_node_dict = {
"id": "host_node",
"uuid": str(uuid.uuid4()),
"parent_uuid": "",
"name": "host_node",
"type": "device",
"class": "host_node",
"config": {},
"data": {},
"children": [],
"description": "",
"schema": {},
"model": {},
"icon": "",
}
# 创建 host_node 的 ResourceTree
host_node_instance = ResourceDictInstance.get_resource_instance_from_dict(host_node_dict)
host_node_tree = ResourceTreeInstance(host_node_instance)
resources_config.trees.insert(0, host_node_tree)
try:
for bridge in self.bridges:
if hasattr(bridge, "resource_tree_add") and resources_config:
from unilabos.app.web.client import HTTPClient
client: HTTPClient = bridge
resource_start_time = time.time()
# 传递 ResourceTreeSet 对象,在 client 中转换为字典并获取 UUID 映射
uuid_mapping = client.resource_tree_add(resources_config, "", True)
resource_end_time = time.time()
self.lab_logger().info(
f"[Host Node-Resource] 物料上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms"
)
for edge in self.resources_edge_config:
edge["source_uuid"] = uuid_mapping.get(edge["source_uuid"], edge["source_uuid"])
edge["target_uuid"] = uuid_mapping.get(edge["target_uuid"], edge["target_uuid"])
resource_add_res = client.resource_edge_add(self.resources_edge_config)
resource_edge_end_time = time.time()
self.lab_logger().info(
f"[Host Node-Resource] 物料关系上传 {round(resource_edge_end_time - resource_end_time, 5) * 1000} ms"
)
# resources_config 通过各个设备的 resource_tracker 进行uuid更新利用uuid_mapping
# resources_config 的 root node 是
# 创建反向映射new_uuid -> old_uuid
reverse_uuid_mapping = {new_uuid: old_uuid for old_uuid, new_uuid in uuid_mapping.items()}
for tree in resources_config.trees:
node = tree.root_node
if node.res_content.type == "device":
for sub_node in node.children:
# 只有二级子设备
if sub_node.res_content.type != "device":
# slave节点走c2s更新接口拿到add自行update uuid
device_tracker = self.devices_instances[node.res_content.id].resource_tracker
# sub_node.res_content.uuid 已经是新UUID需要用旧UUID去查找
old_uuid = reverse_uuid_mapping.get(sub_node.res_content.uuid)
if old_uuid:
# 找到旧UUID使用UUID查找
resource_instance = device_tracker.figure_resource({"uuid": old_uuid})
else:
# 未找到旧UUID使用name查找
resource_instance = device_tracker.figure_resource(
{"name": sub_node.res_content.name}
)
device_tracker.loop_update_uuid(resource_instance, uuid_mapping)
else:
try:
for plr_resource in ResourceTreeSet([tree]).to_plr_resources():
self.resource_tracker.add_resource(plr_resource)
except Exception as ex:
self.lab_logger().warning("[Host Node-Resource] 根节点物料序列化失败!")
except Exception as ex:
self.lab_logger().error("[Host Node-Resource] 添加物料出错!")
self.lab_logger().error(traceback.format_exc())
# 创建定时器,定期发现设备 # 创建定时器,定期发现设备
self._discovery_timer = self.create_timer( self._discovery_timer = self.create_timer(
discovery_interval, self._discovery_devices_callback, callback_group=ReentrantCallbackGroup() discovery_interval, self._discovery_devices_callback, callback_group=ReentrantCallbackGroup()
@@ -862,7 +863,7 @@ class HostNode(BaseROS2DeviceNode):
), ),
} }
def _resource_tree_action_add_callback(self, data: dict, response: SerialCommand_Response): # OK async def _resource_tree_action_add_callback(self, data: dict, response: SerialCommand_Response): # OK
resource_tree_set = ResourceTreeSet.load(data["data"]) resource_tree_set = ResourceTreeSet.load(data["data"])
mount_uuid = data["mount_uuid"] mount_uuid = data["mount_uuid"]
first_add = data["first_add"] first_add = data["first_add"]
@@ -903,7 +904,7 @@ class HostNode(BaseROS2DeviceNode):
response.response = json.dumps(uuid_mapping) if success else "FAILED" response.response = json.dumps(uuid_mapping) if success else "FAILED"
self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}") self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}")
def _resource_tree_action_get_callback(self, data: dict, response: SerialCommand_Response): # OK async def _resource_tree_action_get_callback(self, data: dict, response: SerialCommand_Response): # OK
uuid_list: List[str] = data["data"] uuid_list: List[str] = data["data"]
with_children: bool = data["with_children"] with_children: bool = data["with_children"]
from unilabos.app.web.client import http_client from unilabos.app.web.client import http_client
@@ -911,7 +912,7 @@ class HostNode(BaseROS2DeviceNode):
resource_response = http_client.resource_tree_get(uuid_list, with_children) resource_response = http_client.resource_tree_get(uuid_list, with_children)
response.response = json.dumps(resource_response) response.response = json.dumps(resource_response)
def _resource_tree_action_remove_callback(self, data: dict, response: SerialCommand_Response): async def _resource_tree_action_remove_callback(self, data: dict, response: SerialCommand_Response):
""" """
子节点通知Host物料树删除 子节点通知Host物料树删除
""" """
@@ -919,7 +920,7 @@ class HostNode(BaseROS2DeviceNode):
response.response = "OK" response.response = "OK"
self.lab_logger().info(f"[Host Node-Resource] Resource tree remove completed") self.lab_logger().info(f"[Host Node-Resource] Resource tree remove completed")
def _resource_tree_action_update_callback(self, data: dict, response: SerialCommand_Response): async def _resource_tree_action_update_callback(self, data: dict, response: SerialCommand_Response):
""" """
子节点通知Host物料树更新 子节点通知Host物料树更新
""" """
@@ -932,20 +933,29 @@ class HostNode(BaseROS2DeviceNode):
from unilabos.app.web.client import http_client from unilabos.app.web.client import http_client
resource_start_time = time.time() uuid_to_trees: Dict[str, List[ResourceTreeInstance]] = collections.defaultdict(list)
uuid_mapping = http_client.resource_tree_update(resource_tree_set, "", False) for tree in resource_tree_set.trees:
success = bool(uuid_mapping) uuid_to_trees[tree.root_node.res_content.parent_uuid].append(tree)
resource_end_time = time.time()
self.lab_logger().info(
f"[Host Node-Resource] 物料更新上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms"
)
if uuid_mapping:
self.lab_logger().info(f"[Host Node-Resource] UUID映射: {len(uuid_mapping)} 个节点")
# 还需要加入到资源图中,暂不实现,考虑资源图新的获取方式
response.response = json.dumps(uuid_mapping)
self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}")
def _resource_tree_update_callback(self, request: SerialCommand_Request, response: SerialCommand_Response): for uid, trees in uuid_to_trees.items():
new_tree_set = ResourceTreeSet(trees)
resource_start_time = time.time()
self.lab_logger().info(
f"[Host Node-Resource] 物料 {[root_node.res_content.id for root_node in new_tree_set.root_nodes]} {uid} 挂载 {trees[0].root_node.res_content.parent_uuid} 请求更新上传"
)
uuid_mapping = http_client.resource_tree_add(new_tree_set, uid, False)
success = bool(uuid_mapping)
resource_end_time = time.time()
self.lab_logger().info(
f"[Host Node-Resource] 物料更新上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms"
)
if uuid_mapping:
self.lab_logger().info(f"[Host Node-Resource] UUID映射: {len(uuid_mapping)} 个节点")
# 还需要加入到资源图中,暂不实现,考虑资源图新的获取方式
response.response = json.dumps(uuid_mapping)
self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}")
async def _resource_tree_update_callback(self, request: SerialCommand_Request, response: SerialCommand_Response):
""" """
子节点通知Host物料树更新 子节点通知Host物料树更新
@@ -958,13 +968,13 @@ class HostNode(BaseROS2DeviceNode):
action = data["action"] action = data["action"]
data = data["data"] data = data["data"]
if action == "add": if action == "add":
self._resource_tree_action_add_callback(data, response) await self._resource_tree_action_add_callback(data, response)
elif action == "get": elif action == "get":
self._resource_tree_action_get_callback(data, response) await self._resource_tree_action_get_callback(data, response)
elif action == "update": elif action == "update":
self._resource_tree_action_update_callback(data, response) await self._resource_tree_action_update_callback(data, response)
elif action == "remove": elif action == "remove":
self._resource_tree_action_remove_callback(data, response) await self._resource_tree_action_remove_callback(data, response)
else: else:
self.lab_logger().error(f"[Host Node-Resource] Invalid action: {action}") self.lab_logger().error(f"[Host Node-Resource] Invalid action: {action}")
response.response = "ERROR" response.response = "ERROR"
@@ -1060,7 +1070,12 @@ class HostNode(BaseROS2DeviceNode):
""" """
try: try:
data = json.loads(request.command) data = json.loads(request.command)
http_req = self.bridges[-1].resource_get(data["id"], data["with_children"]) if "uuid" in data and data["uuid"] is not None:
http_req = self.bridges[-1].resource_tree_get([data["uuid"]], data["with_children"])
elif "id" in data and data["id"].startswith("/"):
http_req = self.bridges[-1].resource_get(data["id"], data["with_children"])
else:
raise ValueError("没有使用正确的物料 id 或 uuid")
response.response = json.dumps(http_req["data"]) response.response = json.dumps(http_req["data"])
return response return response
except Exception as e: except Exception as e:

View File

@@ -6,13 +6,14 @@ from typing import List, Dict, Any, Optional, TYPE_CHECKING
import rclpy import rclpy
from rosidl_runtime_py import message_to_ordereddict from rosidl_runtime_py import message_to_ordereddict
from unilabos_msgs.msg import Resource
from unilabos_msgs.srv import ResourceUpdate
from unilabos.messages import * # type: ignore # protocol names from unilabos.messages import * # type: ignore # protocol names
from rclpy.action import ActionServer, ActionClient from rclpy.action import ActionServer, ActionClient
from rclpy.action.server import ServerGoalHandle from rclpy.action.server import ServerGoalHandle
from rclpy.callback_groups import ReentrantCallbackGroup from rclpy.callback_groups import ReentrantCallbackGroup
from unilabos_msgs.msg import Resource # type: ignore from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
from unilabos_msgs.srv import ResourceGet, ResourceUpdate # type: ignore
from unilabos.compile import action_protocol_generators from unilabos.compile import action_protocol_generators
from unilabos.resources.graphio import list_to_nested_dict, nested_dict_to_list from unilabos.resources.graphio import list_to_nested_dict, nested_dict_to_list
@@ -20,11 +21,11 @@ from unilabos.ros.initialize_device import initialize_device_from_dict
from unilabos.ros.msgs.message_converter import ( from unilabos.ros.msgs.message_converter import (
get_action_type, get_action_type,
convert_to_ros_msg, convert_to_ros_msg,
convert_from_ros_msg,
convert_from_ros_msg_with_mapping, convert_from_ros_msg_with_mapping,
) )
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker, ROS2DeviceNode from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, DeviceNodeResourceTracker, ROS2DeviceNode
from unilabos.utils.type_check import serialize_result_info, get_result_info_str from unilabos.ros.nodes.resource_tracker import ResourceTreeSet
from unilabos.utils.type_check import get_result_info_str
if TYPE_CHECKING: if TYPE_CHECKING:
from unilabos.devices.workstation.workstation_base import WorkstationBase from unilabos.devices.workstation.workstation_base import WorkstationBase
@@ -50,6 +51,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
*, *,
driver_instance: "WorkstationBase", driver_instance: "WorkstationBase",
device_id: str, device_id: str,
device_uuid: str,
status_types: Dict[str, Any], status_types: Dict[str, Any],
action_value_mappings: Dict[str, Any], action_value_mappings: Dict[str, Any],
hardware_interface: Dict[str, Any], hardware_interface: Dict[str, Any],
@@ -64,6 +66,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
super().__init__( super().__init__(
driver_instance=driver_instance, driver_instance=driver_instance,
device_id=device_id, device_id=device_id,
device_uuid=device_uuid,
status_types=status_types, status_types=status_types,
action_value_mappings={**action_value_mappings, **self.protocol_action_mappings}, action_value_mappings={**action_value_mappings, **self.protocol_action_mappings},
hardware_interface=hardware_interface, hardware_interface=hardware_interface,
@@ -222,16 +225,29 @@ class ROS2WorkstationNode(BaseROS2DeviceNode):
# 向Host查询物料当前状态 # 向Host查询物料当前状态
for k, v in goal.get_fields_and_field_types().items(): for k, v in goal.get_fields_and_field_types().items():
if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]: if v in ["unilabos_msgs/Resource", "sequence<unilabos_msgs/Resource>"]:
r = ResourceGet.Request() self.lab_logger().info(f"{protocol_name} 查询资源状态: Key: {k} Type: {v}")
resource_id = (
protocol_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else protocol_kwargs[k][0]["id"] try:
) # 统一处理单个或多个资源
r.id = resource_id resource_id = (
r.with_children = True protocol_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else protocol_kwargs[k][0]["id"]
response = await self._resource_clients["resource_get"].call_async(r) )
protocol_kwargs[k] = list_to_nested_dict( resource_uuid = protocol_kwargs[k].get("uuid", None)
[convert_from_ros_msg(rs) for rs in response.resources] r = SerialCommand_Request()
) r.command = json.dumps({"id": resource_id, "uuid": resource_uuid, "with_children": True})
# 发送请求并等待响应
response: SerialCommand_Response = await self._resource_clients[
"resource_get"
].call_async(
r
) # type: ignore
raw_data = json.loads(response.response)
tree_set = ResourceTreeSet.from_raw_list(raw_data)
target = tree_set.dump()
protocol_kwargs[k] = target[0][0] if v == "unilabos_msgs/Resource" else target
except Exception as ex:
self.lab_logger().error(f"查询资源失败: {k}, 错误: {ex}\n{traceback.format_exc()}")
raise
self.lab_logger().info(f"🔍 最终的 vessel: {protocol_kwargs.get('vessel', 'NOT_FOUND')}") self.lab_logger().info(f"🔍 最终的 vessel: {protocol_kwargs.get('vessel', 'NOT_FOUND')}")

View File

@@ -1,3 +1,4 @@
import traceback
import uuid import uuid
from pydantic import BaseModel, field_serializer, field_validator from pydantic import BaseModel, field_serializer, field_validator
from pydantic import Field from pydantic import Field
@@ -31,7 +32,7 @@ class ResourceDictPositionObject(BaseModel):
class ResourceDictPosition(BaseModel): class ResourceDictPosition(BaseModel):
size: ResourceDictPositionSize = Field(description="Resource size", default_factory=ResourceDictPositionSize) size: ResourceDictPositionSize = Field(description="Resource size", default_factory=ResourceDictPositionSize)
scale: ResourceDictPositionScale = Field(description="Resource scale", default_factory=ResourceDictPositionScale) scale: ResourceDictPositionScale = Field(description="Resource scale", default_factory=ResourceDictPositionScale)
layout: Literal["2d"] = Field(description="Resource layout", default="2d") layout: Literal["2d", "x-y", "z-y", "x-z"] = Field(description="Resource layout", default="x-y")
position: ResourceDictPositionObject = Field( position: ResourceDictPositionObject = Field(
description="Resource position", default_factory=ResourceDictPositionObject description="Resource position", default_factory=ResourceDictPositionObject
) )
@@ -41,6 +42,7 @@ class ResourceDictPosition(BaseModel):
rotation: ResourceDictPositionObject = Field( rotation: ResourceDictPositionObject = Field(
description="Resource rotation", default_factory=ResourceDictPositionObject description="Resource rotation", default_factory=ResourceDictPositionObject
) )
cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"] = Field(description="Cross section type", default="rectangle")
# 统一的资源字典模型parent 自动序列化为 parent_uuidchildren 不序列化 # 统一的资源字典模型parent 自动序列化为 parent_uuidchildren 不序列化
@@ -49,7 +51,7 @@ class ResourceDict(BaseModel):
uuid: str = Field(description="Resource UUID") uuid: str = Field(description="Resource UUID")
name: str = Field(description="Resource name") name: str = Field(description="Resource name")
description: str = Field(description="Resource description", default="") description: str = Field(description="Resource description", default="")
schema: Dict[str, Any] = Field(description="Resource schema", default_factory=dict) resource_schema: Dict[str, Any] = Field(description="Resource schema", default_factory=dict, serialization_alias="schema", validation_alias="schema")
model: Dict[str, Any] = Field(description="Resource model", default_factory=dict) model: Dict[str, Any] = Field(description="Resource model", default_factory=dict)
icon: str = Field(description="Resource icon", default="") icon: str = Field(description="Resource icon", default="")
parent_uuid: Optional["str"] = Field(description="Parent resource uuid", default=None) # 先设定parent_uuid parent_uuid: Optional["str"] = Field(description="Parent resource uuid", default=None) # 先设定parent_uuid
@@ -57,6 +59,7 @@ class ResourceDict(BaseModel):
type: Literal["device"] | str = Field(description="Resource type") type: Literal["device"] | str = Field(description="Resource type")
klass: str = Field(alias="class", description="Resource class name") klass: str = Field(alias="class", description="Resource class name")
position: ResourceDictPosition = Field(description="Resource position", default_factory=ResourceDictPosition) position: ResourceDictPosition = Field(description="Resource position", default_factory=ResourceDictPosition)
pose: ResourceDictPosition = Field(description="Resource position", default_factory=ResourceDictPosition)
config: Dict[str, Any] = Field(description="Resource configuration") config: Dict[str, Any] = Field(description="Resource configuration")
data: Dict[str, Any] = Field(description="Resource data") data: Dict[str, Any] = Field(description="Resource data")
@@ -135,12 +138,14 @@ class ResourceDictInstance(object):
content["config"] = {} content["config"] = {}
if not content.get("data"): if not content.get("data"):
content["data"] = {} content["data"] = {}
if "pose" not in content:
content["pose"] = content.get("position", {})
return ResourceDictInstance(ResourceDict.model_validate(content)) return ResourceDictInstance(ResourceDict.model_validate(content))
def get_nested_dict(self) -> Dict[str, Any]: def get_nested_dict(self) -> Dict[str, Any]:
"""获取资源实例的嵌套字典表示""" """获取资源实例的嵌套字典表示"""
res_dict = self.res_content.model_dump(by_alias=True) res_dict = self.res_content.model_dump(by_alias=True)
res_dict["children"] = {child.res_content.name: child.get_nested_dict() for child in self.children} res_dict["children"] = {child.res_content.id: child.get_nested_dict() for child in self.children}
res_dict["parent"] = self.res_content.parent_instance_name res_dict["parent"] = self.res_content.parent_instance_name
res_dict["position"] = self.res_content.position.position.model_dump() res_dict["position"] = self.res_content.position.position.model_dump()
return res_dict return res_dict
@@ -213,7 +218,7 @@ class ResourceTreeInstance(object):
if node.res_content.uuid: if node.res_content.uuid:
known_uuids.add(node.res_content.uuid) known_uuids.add(node.res_content.uuid)
else: else:
print(f"警告: 资源 {node.res_content.id} 没有uuid") logger.warning(f"警告: 资源 {node.res_content.id} 没有uuid")
# 验证并递归处理子节点 # 验证并递归处理子节点
for child in node.children: for child in node.children:
@@ -289,8 +294,6 @@ class ResourceTreeSet(object):
elif isinstance(resource_list[0], ResourceTreeInstance): elif isinstance(resource_list[0], ResourceTreeInstance):
# 已经是ResourceTree列表 # 已经是ResourceTree列表
self.trees = cast(List[ResourceTreeInstance], resource_list) self.trees = cast(List[ResourceTreeInstance], resource_list)
elif isinstance(resource_list[0], list):
pass
else: else:
raise TypeError( raise TypeError(
f"不支持的类型: {type(resource_list[0])}" f"不支持的类型: {type(resource_list[0])}"
@@ -307,10 +310,7 @@ class ResourceTreeSet(object):
replace_info = { replace_info = {
"plate": "plate", "plate": "plate",
"well": "well", "well": "well",
"tip_spot": "container",
"trash": "container",
"deck": "deck", "deck": "deck",
"tip_rack": "container",
} }
if source in replace_info: if source in replace_info:
return replace_info[source] return replace_info[source]
@@ -320,7 +320,12 @@ class ResourceTreeSet(object):
def build_uuid_mapping(res: "PLRResource", uuid_list: list): def build_uuid_mapping(res: "PLRResource", uuid_list: list):
"""递归构建uuid映射字典""" """递归构建uuid映射字典"""
uuid_list.append(getattr(res, "unilabos_uuid", "")) uid = getattr(res, "unilabos_uuid", "")
if not uid:
uid = str(uuid.uuid4())
res.unilabos_uuid = uid
logger.warning(f"{res}没有uuid请设置后再传入默认填充{uid}\n{traceback.format_exc()}")
uuid_list.append(uid)
for child in res.children: for child in res.children:
build_uuid_mapping(child, uuid_list) build_uuid_mapping(child, uuid_list)
@@ -329,6 +334,21 @@ class ResourceTreeSet(object):
) -> ResourceDictInstance: ) -> ResourceDictInstance:
current_uuid = uuids.pop(0) current_uuid = uuids.pop(0)
raw_pos = (
{"x": d["location"]["x"], "y": d["location"]["y"], "z": d["location"]["z"]}
if d["location"]
else {"x": 0, "y": 0, "z": 0}
)
pos = {
"size": {"width": d["size_x"], "height": d["size_y"], "depth": d["size_z"]},
"scale": {"x": 1.0, "y": 1.0, "z": 1.0},
"layout": d.get("layout", "x-y"),
"position": raw_pos,
"position3d": raw_pos,
"rotation": d["rotation"],
"cross_section_type": d.get("cross_section_type", "rectangle"),
}
# 先构建当前节点的字典不包含children # 先构建当前节点的字典不包含children
r_dict = { r_dict = {
"id": d["name"], "id": d["name"],
@@ -337,12 +357,10 @@ class ResourceTreeSet(object):
"parent": parent_resource, # 直接传入 ResourceDict 对象 "parent": parent_resource, # 直接传入 ResourceDict 对象
"type": replace_plr_type(d.get("category", "")), "type": replace_plr_type(d.get("category", "")),
"class": d.get("class", ""), "class": d.get("class", ""),
"position": ( "position": pos,
{"x": d["location"]["x"], "y": d["location"]["y"], "z": d["location"]["z"]} "pose": pos,
if d["location"] "config": {k: v for k, v in d.items() if k not in
else {"x": 0, "y": 0, "z": 0} ["name", "children", "parent_name", "location", "rotation", "size_x", "size_y", "size_z", "cross_section_type", "bottom_type"]},
),
"config": {k: v for k, v in d.items() if k not in ["name", "children", "parent_name", "location"]},
"data": states[d["name"]], "data": states[d["name"]],
} }
@@ -384,7 +402,7 @@ class ResourceTreeSet(object):
import inspect import inspect
# 类型映射 # 类型映射
TYPE_MAP = {"plate": "plate", "well": "well", "container": "tip_spot", "deck": "deck", "tip_rack": "tip_rack"} TYPE_MAP = {"plate": "Plate", "well": "Well", "deck": "Deck"}
def collect_node_data(node: ResourceDictInstance, name_to_uuid: dict, all_states: dict): def collect_node_data(node: ResourceDictInstance, name_to_uuid: dict, all_states: dict):
"""一次遍历收集 name_to_uuid 和 all_states""" """一次遍历收集 name_to_uuid 和 all_states"""
@@ -396,13 +414,13 @@ class ResourceTreeSet(object):
def node_to_plr_dict(node: ResourceDictInstance, has_model: bool): def node_to_plr_dict(node: ResourceDictInstance, has_model: bool):
"""转换节点为 PLR 字典格式""" """转换节点为 PLR 字典格式"""
res = node.res_content res = node.res_content
plr_type = TYPE_MAP.get(res.type, "tip_spot") plr_type = TYPE_MAP.get(res.type, res.type)
if res.type not in TYPE_MAP: if res.type not in TYPE_MAP:
logger.warning(f"未知类型 {res.type},使用默认类型 tip_spot") logger.warning(f"未知类型 {res.type}")
d = { d = {
"name": res.name, "name": res.name,
"type": plr_type, "type": res.config.get("type", plr_type),
"size_x": res.config.get("size_x", 0), "size_x": res.config.get("size_x", 0),
"size_y": res.config.get("size_y", 0), "size_y": res.config.get("size_y", 0),
"size_z": res.config.get("size_z", 0), "size_z": res.config.get("size_z", 0),
@@ -413,7 +431,7 @@ class ResourceTreeSet(object):
"type": "Coordinate", "type": "Coordinate",
}, },
"rotation": {"x": 0, "y": 0, "z": 0, "type": "Rotation"}, "rotation": {"x": 0, "y": 0, "z": 0, "type": "Rotation"},
"category": plr_type, "category": res.config.get("category", plr_type),
"children": [node_to_plr_dict(child, has_model) for child in node.children], "children": [node_to_plr_dict(child, has_model) for child in node.children],
"parent_name": res.parent_instance_name, "parent_name": res.parent_instance_name,
**res.config, **res.config,
@@ -435,7 +453,7 @@ class ResourceTreeSet(object):
try: try:
sub_cls = find_subclass(plr_dict["type"], PLRResource) sub_cls = find_subclass(plr_dict["type"], PLRResource)
if sub_cls is None: if sub_cls is None:
raise ValueError(f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类") raise ValueError(f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类。原始信息:{tree.root_node.res_content}")
spec = inspect.signature(sub_cls) spec = inspect.signature(sub_cls)
if "category" not in spec.parameters: if "category" not in spec.parameters:
plr_dict.pop("category", None) plr_dict.pop("category", None)
@@ -715,16 +733,9 @@ class ResourceTreeSet(object):
Returns: Returns:
ResourceTreeSet: 反序列化后的资源树集合 ResourceTreeSet: 反序列化后的资源树集合
""" """
# 将每个字典转换为 ResourceInstanceDict
# FIXME: 需要重新确定parent关系
nested_lists = [] nested_lists = []
for tree_data in data: for tree_data in data:
flatten_instances = [ nested_lists.extend(ResourceTreeSet.from_raw_list(tree_data).trees)
ResourceDictInstance.get_resource_instance_from_dict(node_dict) for node_dict in tree_data
]
nested_lists.append(flatten_instances)
# 使用现有的构造函数创建 ResourceTreeSet
return cls(nested_lists) return cls(nested_lists)
@@ -777,7 +788,8 @@ class DeviceNodeResourceTracker(object):
else: else:
return getattr(resource, uuid_attr, None) return getattr(resource, uuid_attr, None)
def _set_resource_uuid(self, resource, new_uuid: str): @classmethod
def set_resource_uuid(cls, resource, new_uuid: str):
""" """
设置资源的 uuid统一处理 dict 和 instance 两种类型 设置资源的 uuid统一处理 dict 和 instance 两种类型
@@ -830,7 +842,7 @@ class DeviceNodeResourceTracker(object):
resource_name = self._get_resource_attr(res, "name") resource_name = self._get_resource_attr(res, "name")
if resource_name and resource_name in name_to_uuid_map: if resource_name and resource_name in name_to_uuid_map:
new_uuid = name_to_uuid_map[resource_name] new_uuid = name_to_uuid_map[resource_name]
self._set_resource_uuid(res, new_uuid) self.set_resource_uuid(res, new_uuid)
self.uuid_to_resources[new_uuid] = res self.uuid_to_resources[new_uuid] = res
logger.debug(f"设置资源UUID: {resource_name} -> {new_uuid}") logger.debug(f"设置资源UUID: {resource_name} -> {new_uuid}")
return 1 return 1
@@ -842,7 +854,7 @@ class DeviceNodeResourceTracker(object):
""" """
递归遍历资源树更新所有节点的uuid 递归遍历资源树更新所有节点的uuid
Args: Args:0
resource: 资源对象可以是dict或实例 resource: 资源对象可以是dict或实例
uuid_map: uuid映射字典{old_uuid: new_uuid} uuid_map: uuid映射字典{old_uuid: new_uuid}
@@ -852,17 +864,18 @@ class DeviceNodeResourceTracker(object):
def process(res): def process(res):
current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid") current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid")
replaced = 0
if current_uuid and current_uuid in uuid_map: if current_uuid and current_uuid in uuid_map:
new_uuid = uuid_map[current_uuid] new_uuid = uuid_map[current_uuid]
if current_uuid != new_uuid: if current_uuid != new_uuid:
self._set_resource_uuid(res, new_uuid) self.set_resource_uuid(res, new_uuid)
# 更新uuid_to_resources映射 # 更新uuid_to_resources映射
if current_uuid in self.uuid_to_resources: if current_uuid in self.uuid_to_resources:
self.uuid_to_resources.pop(current_uuid) self.uuid_to_resources.pop(current_uuid)
self.uuid_to_resources[new_uuid] = res self.uuid_to_resources[new_uuid] = res
logger.debug(f"更新uuid: {current_uuid} -> {new_uuid}") logger.debug(f"更新uuid: {current_uuid} -> {new_uuid}")
return 1 replaced = 1
return 0 return replaced
return self._traverse_and_process(resource, process) return self._traverse_and_process(resource, process)
@@ -877,8 +890,9 @@ class DeviceNodeResourceTracker(object):
def process(res): def process(res):
current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid") current_uuid = self._get_resource_attr(res, "uuid", "unilabos_uuid")
if current_uuid: if current_uuid:
old = self.uuid_to_resources.get(current_uuid)
self.uuid_to_resources[current_uuid] = res self.uuid_to_resources[current_uuid] = res
logger.debug(f"收集资源UUID映射: {current_uuid} -> {res}") logger.debug(f"收集资源UUID映射: {current_uuid} -> {res} {'' if old is None else f'(覆盖旧值: {old})'}")
return 0 return 0
self._traverse_and_process(resource, process) self._traverse_and_process(resource, process)
@@ -913,9 +927,23 @@ class DeviceNodeResourceTracker(object):
Args: Args:
resource: 资源对象可以是dict或实例 resource: 资源对象可以是dict或实例
""" """
root_uuids = {}
for r in self.resources: for r in self.resources:
res_uuid = r.get("uuid") if isinstance(r, dict) else getattr(r, "unilabos_uuid", None)
if res_uuid:
root_uuids[res_uuid] = r
if id(r) == id(resource): if id(r) == id(resource):
return return
# 这里只做uuid的根节点比较
if isinstance(resource, dict):
res_uuid = resource.get("uuid")
else:
res_uuid = getattr(resource, "unilabos_uuid", None)
if res_uuid in root_uuids:
old_res = root_uuids[res_uuid]
# self.remove_resource(old_res)
logger.warning(f"资源{resource}已存在,旧资源: {old_res}")
self.resources.append(resource) self.resources.append(resource)
# 递归收集uuid映射 # 递归收集uuid映射
self._collect_uuid_mapping(resource) self._collect_uuid_mapping(resource)
@@ -1046,13 +1074,19 @@ class DeviceNodeResourceTracker(object):
) -> List[Tuple[Any, Any]]: ) -> List[Tuple[Any, Any]]:
res_list = [] res_list = []
# print(resource, target_resource_cls_type, identifier_key, compare_value) # print(resource, target_resource_cls_type, identifier_key, compare_value)
children = getattr(resource, "children", []) children = []
if not isinstance(resource, dict):
children = getattr(resource, "children", [])
else:
children = resource.get("children")
if children is not None:
children = list(children.values()) if isinstance(children, dict) else children
for child in children: for child in children:
res_list.extend( res_list.extend(
self.loop_find_resource(child, target_resource_cls_type, identifier_key, compare_value, resource) self.loop_find_resource(child, target_resource_cls_type, identifier_key, compare_value, resource)
) )
if issubclass(type(resource), target_resource_cls_type): if issubclass(type(resource), target_resource_cls_type):
if target_resource_cls_type == dict: if type(resource) == dict:
# 对于字典类型,直接检查 identifier_key # 对于字典类型,直接检查 identifier_key
if identifier_key in resource: if identifier_key in resource:
if resource[identifier_key] == compare_value: if resource[identifier_key] == compare_value:

View File

@@ -336,6 +336,9 @@ class WorkstationNodeCreator(DeviceClassCreator[T]):
try: try:
# 创建实例额外补充一个给protocol node的字段后面考虑取消 # 创建实例额外补充一个给protocol node的字段后面考虑取消
data["children"] = self.children data["children"] = self.children
for material_id, child in self.children.items():
if child["type"] != "device":
self.resource_tracker.add_resource(self.children[material_id])
deck_dict = data.get("deck") deck_dict = data.get("deck")
if deck_dict: if deck_dict:
from pylabrobot.resources import Deck, Resource from pylabrobot.resources import Deck, Resource