Compare commits

...

201 Commits

Author SHA1 Message Date
Xuwznln
ac88c59b50 protocol node不再嵌套显示 2025-06-17 16:10:28 +08:00
Xuwznln
2492af57c0 完善tip 2025-06-17 15:59:32 +08:00
Xuwznln
b1dae6da17 默认不进行注册表报送,通过命令unilabos-register或者增加启动参数 2025-06-17 15:58:56 +08:00
Junhan Chang
af812d630a separate registry sync and resource_add 2025-06-17 15:37:36 +08:00
Xuwznln
183579fd7f 移除device的父节点关联 2025-06-17 15:34:03 +08:00
Junhan Chang
678ace6109 Fix edge id 2025-06-17 13:53:38 +08:00
Junhan Chang
18c4eb3e4d fix device ports 2025-06-17 13:27:27 +08:00
Junhan Chang
dd7abe987e fix resource and edge upload 2025-06-17 13:27:01 +08:00
KCFeng425
3e6c8d6340 修改了json图中link的格式 2025-06-17 13:01:55 +08:00
KCFeng425
ab7f1539af Merge branch 'device-registry-port' of github.com:KCFeng425/Uni-Lab-OS into device-registry-port 2025-06-17 10:00:19 +08:00
KCFeng425
ee918a0e93 添加了icon的文件名在注册表里面 2025-06-17 10:00:11 +08:00
Xuwznln
6fd95fdb08 default resource upload mode is false 2025-06-16 16:08:18 +08:00
KCFeng425
31993594e6 大图的问题都修复好了,添加了gassource和vacuum pump的驱动以及注册表 2025-06-16 14:39:55 +08:00
Xuwznln
6c471553c4 fix container value
add parent_name to edge device id
2025-06-16 12:45:26 +08:00
Xuwznln
e193bc493c Merge branch 'device-registry-port' of https://github.com/KCFeng425/Uni-Lab-OS into fork/KCFeng425/device-registry-port 2025-06-16 12:16:05 +08:00
Xuwznln
9f8f6e55c4 add virtual_separator virtual_rotavap
fix transfer_pump
2025-06-16 12:15:54 +08:00
Junhan Chang
8d56c523bb update container registry and handles 2025-06-16 12:15:39 +08:00
Xuwznln
57cb120c8c add resource edge upload 2025-06-16 11:51:02 +08:00
Xuwznln
a303bd7c5b bump version to 0.9.6 2025-06-16 10:59:35 +08:00
Xuwznln
47e58e13c7 Merge remote-tracking branch 'origin/dev' into fork/KCFeng425/device-registry-port 2025-06-16 10:01:27 +08:00
Junhan Chang
5b9e13555c Fix handles 2025-06-16 08:03:06 +08:00
KCFeng425
7b04f3fa50 修复了添加protocol前缀导致的不能启动的bug 2025-06-16 02:06:53 +08:00
Xuwznln
f7db8d17c5 container 添加和更新完成 2025-06-15 17:37:38 +08:00
KCFeng425
ff6998501e 修复了阀门更新版的bug 2025-06-15 13:55:51 +08:00
Xuwznln
a354965f8e Merge branch 'dev' of https://github.com/dptech-corp/Uni-Lab-OS into dev 2025-06-15 12:51:48 +08:00
Xuwznln
934276d2f7 create container 2025-06-15 12:51:37 +08:00
Harvey Que
803809480b hotfix: Add .certs in .gitignore 2025-06-15 09:09:06 +08:00
KCFeng425
b875f86bbb 修改了add protocol 2025-06-14 18:53:43 +08:00
KCFeng425
d058de3702 区分了虚拟仪器中的八通阀和电磁阀,添加了两个阀门的驱动 2025-06-14 16:23:26 +08:00
KCFeng425
6385065ba3 更正了stir和heater的连接方式 2025-06-14 15:58:52 +08:00
KCFeng425
3b32dcf066 Update virtual_device.yaml 2025-06-14 15:48:56 +08:00
KCFeng425
7c714721db Merge branch 'dev' of github.com:dptech-corp/Uni-Lab-OS into dev 2025-06-14 14:44:46 +08:00
Xuwznln
5478ba3237 test artifacts 2025-06-13 14:13:41 +08:00
Xuwznln
49f1aa9c28 try build 2025-06-13 14:05:58 +08:00
Xuwznln
d5d516f0ef try build fix 2025-06-13 13:52:45 +08:00
Xuwznln
4471fed4b8 测试自动构建 2025-06-13 13:47:08 +08:00
Xuwznln
30d143e1a5 Merge branch 'dev' of https://github.com/dptech-corp/Uni-Lab-OS into dev 2025-06-13 13:12:46 +08:00
Xuwznln
75ea45f21e include device_mesh when pip install 2025-06-13 00:32:15 +08:00
hh.
66af337d6c hotfix: Add macos_sdk_config (#46)
Co-authored-by: quehh <scienceol@outlook.com>
2025-06-12 22:46:44 +08:00
Xuwznln
ae3c65c1d3 Merge remote-tracking branch 'origin/main' into dev
# Conflicts:
#	README.md
#	README_zh.md
#	recipes/ros-humble-unilabos-msgs/recipe.yaml
#	recipes/unilabos/recipe.yaml
#	setup.py
#	unilabos/compile/pump_protocol.py
#	unilabos/registry/devices/pump_and_valve.yaml
#	unilabos/ros/nodes/presets/protocol_node.py
2025-06-12 21:27:07 +08:00
Xuwznln
11e4f053f1 bump version & protocol fix 2025-06-12 21:21:25 +08:00
Kongchang Feng
96f37b3b0d Add Mock Device for Organic Synthesis\添加有机合成的虚拟仪器和Protocol (#43)
* Add Device MockChiller

Add device MockChiller

* Add Device MockFilter

* Add Device MockPump

* Add Device MockRotavap

* Add Device MockSeparator

* Add Device MockStirrer

* Add Device MockHeater

* Add Device MockVacuum

* Add Device MockSolenoidValve

* Add Device Mock \_init_.py

* 规范模拟设备代码与注册表信息

* 更改Mock大写文件夹名

* 删除大写目录

* Edited Mock device json

* Match mock device with action

* Edit mock device yaml

* Add new action

* Add Virtual Device, Action, YAML, Protocol for Organic Syn

* 单独分类测试的protocol文件夹

* 更名Action

---------

Co-authored-by: Xuwznln <18435084+Xuwznln@users.noreply.github.com>
2025-06-12 20:58:39 +08:00
Xuwznln
d7d0a27976 Device visualization (#42)
* Update README and MQTTClient for installation instructions and code improvements

* feat: 支持local_config启动
add: 增加对crt path的说明,为传入config.py的相对路径
move: web component

* add: registry description

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* feat: node_info_update srv
fix: OTDeck cant create

* close #12
feat: slave node registry

* feat: show machine name
fix: host node registry not uploaded

* feat: add hplc registry

* feat: add hplc registry

* fix: hplc status typo

* fix: devices/

* 完成启动OT并联动rviz

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* fix: device.class possible null

* fix: HPLC additions with online service

* fix: slave mode spin not working

* fix: slave mode spin not working

* 修复rviz位置问题,

修复rviz位置问题,
在无tf变动时减缓发送频率
在backend中添加物料跟随方法

* feat: 多ProtocolNode 允许子设备ID相同
feat: 上报发现的ActionClient
feat: Host重启动,通过discover机制要求slaveNode重新注册,实现信息及时上报

* feat: 支持env设置config

* fix: running logic

* fix: running logic

* fix: missing ot

* 在main中直接初始化republisher和物料的mesh节点

* 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中

* Device visualization (#14)

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* 修复rviz位置问题,

修复rviz位置问题,
在无tf变动时减缓发送频率
在backend中添加物料跟随方法

* fix: running logic

* fix: running logic

* fix: missing ot

* 在main中直接初始化republisher和物料的mesh节点

* 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中

---------

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

* fix: missing hostname in devices_names
fix: upload_file for model file

* fix: missing paho-mqtt package
bump version to 0.9.0

* fix startup
add ResourceCreateFromOuter.action

* fix type hint

* update actions

* update actions

* host node add_resource_from_outer
fix cmake list

* pass device config to device class

* add: bind_parent_ids to resource create action
fix: message convert string

* fix: host node should not be re_discovered

* feat: resource tracker support dict

* feat: add more necessary params

* feat: fix boolean null in registry action data

* feat: add outer resource

* 编写mesh添加action

* feat: append resource

* add action

* feat: vis 2d for plr

* fix

* fix: browser on rviz

* fix: cloud bridge error fallback to local

* fix: salve auto run rviz

* 初始化两个plate

* Device visualization (#22)

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* 修复rviz位置问题,

修复rviz位置问题,
在无tf变动时减缓发送频率
在backend中添加物料跟随方法

* fix: running logic

* fix: running logic

* fix: missing ot

* 在main中直接初始化republisher和物料的mesh节点

* 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中

* 编写mesh添加action

* add action

* fix

* fix: browser on rviz

* fix: cloud bridge error fallback to local

* fix: salve auto run rviz

* 初始化两个plate

---------

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

* fix: multi channel

* fix: aspirate

* fix: aspirate

* fix: aspirate

* fix: aspirate

* 提交

* fix: jobadd

* fix: jobadd

* fix: msg converter

* tijiao

* add resource creat easy action

* identify debug msg

* mq client id

* 提取lh的joint发布

* unify liquid_handler definition

* 修改物料跟随与物料添加逻辑

修改物料跟随与物料添加逻辑
将joint_publisher类移出lh的backends,但仍需要对lh的backends进行一些改写

* Revert "修改物料跟随与物料添加逻辑"

This reverts commit 498c997ad7.

* Reapply "修改物料跟随与物料添加逻辑"

This reverts commit 3a60d2ae81.

* Revert "Merge remote-tracking branch 'upstream/dev' into device_visualization"

This reverts commit fa727220af, reversing
changes made to 498c997ad7.

* 修改物料放下时的方法,如果选择

修改物料放下时的方法,
如果选择drop_trash,则删除物料显示
如果选择drop,则让其解除连接

* unilab添加moveit启动

1,整合所有moveit节点到一个move_group中,并整合所有的controller依次激活
2,添加pymoveit2的节点,使用json可直接启动
3,修改机械臂规划方式,添加约束,让冗余关节不会进行过多移动

* 修改物体attach时,多次赋值当前时间导致卡顿问题,

* Revert "修改物体attach时,多次赋值当前时间导致卡顿问题,"

This reverts commit 56d45b94f5.

* Reapply "修改物体attach时,多次赋值当前时间导致卡顿问题,"

This reverts commit 07d9db20c3.

* 添加缺少物料:"plate_well_G12",

* add

* fix tip resource data

* liquid states

* change to debug level

* Revert "change to debug level"

This reverts commit 5d9953c3e5.

* Reapply "change to debug level"

This reverts commit 2487bb6ffc.

* fix tip resource data

* add full device

* add moveit yaml

* 修复moveit
增加post_init阶段,给予ros_node反向

* remove necessary node

* fix moveit action client

* remove necessary imports

* Update moveit_interface.py

* fix handler_key uppercase

* json add liquids

* fix setup

* add

* change to "sources" and "targets" for lh

* bump version

* remove parent's parent link

* change arm's name

* change name

* fix ik error

---------

Co-authored-by: Harvey Que <Q-Query@outlook.com>
Co-authored-by: zhangshixiang <@zhangshixiang>
Co-authored-by: q434343 <73513873+q434343@users.noreply.github.com>
Co-authored-by: Junhan Chang <changjh@pku.edu.cn>
2025-06-12 20:58:18 +08:00
Xuwznln
34151f5cb2 补充日志 2025-06-10 22:13:35 +08:00
Xuwznln
369a21b904 调整protocol node以更好支持多种类型的read和write 2025-06-10 21:54:23 +08:00
Xuwznln
90169981c1 增加modbus支持
调整protocol node以更好支持多种类型的read和write
2025-06-10 21:46:49 +08:00
Xuwznln
d297abfd19 bump ver
modify slot type
2025-06-10 03:46:28 +08:00
Xuwznln
9c515a252a create_resource 2025-06-10 02:55:29 +08:00
Xuwznln
ea5e7a5ce2 Merge branch '37-biomek-i5i7' into dev
# Conflicts:
#	README.md
#	README_zh.md
#	recipes/ros-humble-unilabos-msgs/recipe.yaml
#	recipes/unilabos/recipe.yaml
#	setup.py
#	unilabos/devices/liquid_handling/biomek.py
#	unilabos/devices/liquid_handling/biomek_test.py
#	unilabos/registry/devices/liquid_handler.yaml
#	unilabos/registry/registry.py
#	unilabos/ros/msgs/message_converter.py
#	unilabos_msgs/action/LiquidHandlerMoveBiomek.action
#	unilabos_msgs/action/LiquidHandlerTransferBiomek.action
2025-06-10 02:00:43 +08:00
Xuwznln
2e9a0a4677 fix move it 2025-06-10 01:55:39 +08:00
Xuwznln
4c7aa8a89a fix move it 2025-06-10 01:53:58 +08:00
Xuwznln
d8a0c5e715 Device visualization (#41)
* Update README and MQTTClient for installation instructions and code improvements

* feat: 支持local_config启动
add: 增加对crt path的说明,为传入config.py的相对路径
move: web component

* add: registry description

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* feat: node_info_update srv
fix: OTDeck cant create

* close #12
feat: slave node registry

* feat: show machine name
fix: host node registry not uploaded

* feat: add hplc registry

* feat: add hplc registry

* fix: hplc status typo

* fix: devices/

* 完成启动OT并联动rviz

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* fix: device.class possible null

* fix: HPLC additions with online service

* fix: slave mode spin not working

* fix: slave mode spin not working

* 修复rviz位置问题,

修复rviz位置问题,
在无tf变动时减缓发送频率
在backend中添加物料跟随方法

* feat: 多ProtocolNode 允许子设备ID相同
feat: 上报发现的ActionClient
feat: Host重启动,通过discover机制要求slaveNode重新注册,实现信息及时上报

* feat: 支持env设置config

* fix: running logic

* fix: running logic

* fix: missing ot

* 在main中直接初始化republisher和物料的mesh节点

* 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中

* Device visualization (#14)

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* 修复rviz位置问题,

修复rviz位置问题,
在无tf变动时减缓发送频率
在backend中添加物料跟随方法

* fix: running logic

* fix: running logic

* fix: missing ot

* 在main中直接初始化republisher和物料的mesh节点

* 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中

---------

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

* fix: missing hostname in devices_names
fix: upload_file for model file

* fix: missing paho-mqtt package
bump version to 0.9.0

* fix startup
add ResourceCreateFromOuter.action

* fix type hint

* update actions

* update actions

* host node add_resource_from_outer
fix cmake list

* pass device config to device class

* add: bind_parent_ids to resource create action
fix: message convert string

* fix: host node should not be re_discovered

* feat: resource tracker support dict

* feat: add more necessary params

* feat: fix boolean null in registry action data

* feat: add outer resource

* 编写mesh添加action

* feat: append resource

* add action

* feat: vis 2d for plr

* fix

* fix: browser on rviz

* fix: cloud bridge error fallback to local

* fix: salve auto run rviz

* 初始化两个plate

* Device visualization (#22)

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* 修复rviz位置问题,

修复rviz位置问题,
在无tf变动时减缓发送频率
在backend中添加物料跟随方法

* fix: running logic

* fix: running logic

* fix: missing ot

* 在main中直接初始化republisher和物料的mesh节点

* 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中

* 编写mesh添加action

* add action

* fix

* fix: browser on rviz

* fix: cloud bridge error fallback to local

* fix: salve auto run rviz

* 初始化两个plate

---------

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

* fix: multi channel

* fix: aspirate

* fix: aspirate

* fix: aspirate

* fix: aspirate

* 提交

* fix: jobadd

* fix: jobadd

* fix: msg converter

* tijiao

* add resource creat easy action

* identify debug msg

* mq client id

* 提取lh的joint发布

* unify liquid_handler definition

* 修改物料跟随与物料添加逻辑

修改物料跟随与物料添加逻辑
将joint_publisher类移出lh的backends,但仍需要对lh的backends进行一些改写

* Revert "修改物料跟随与物料添加逻辑"

This reverts commit 498c997ad7.

* Reapply "修改物料跟随与物料添加逻辑"

This reverts commit 3a60d2ae81.

* Revert "Merge remote-tracking branch 'upstream/dev' into device_visualization"

This reverts commit fa727220af, reversing
changes made to 498c997ad7.

* 修改物料放下时的方法,如果选择

修改物料放下时的方法,
如果选择drop_trash,则删除物料显示
如果选择drop,则让其解除连接

* add biomek.py demo implementation

* 更新LiquidHandlerBiomek类,添加资源创建功能,优化协议创建方法,修复部分代码格式问题,更新YAML配置以支持新功能。

* Test

* fix biomek success type

* Convert LH action to biomek.

* Update biomek.py

* 注册表上报handle和schema (param input)

* 修复biomek缺少的字段

* delete 's'

* Remove warnings

* Update biomek.py

* Biomek test

* Update biomek.py

* 新增transfer_biomek的msg

* New transfer_biomek

* Updated transfer_biomek

* 更新transfer_biomek的msg

* 更新transfer_biomek的msg

* 支持Biomek创建

* new action

* fix key name typo

* New parameter for biomek to run.

* Refine

* Update

* new actions

* new actions

* 1

* registry

* fix biomek startup
add action handles

* fix handles not as default entry

* unilab添加moveit启动

1,整合所有moveit节点到一个move_group中,并整合所有的controller依次激活
2,添加pymoveit2的节点,使用json可直接启动
3,修改机械臂规划方式,添加约束,让冗余关节不会进行过多移动

* biomek_test.py

biomek_test.py是最新的版本,运行它会生成complete_biomek_protocol.json

* Update biomek.py

* biomek_test.py

* fix liquid_handler.biomek handles

* 修改物体attach时,多次赋值当前时间导致卡顿问题,

* Revert "修改物体attach时,多次赋值当前时间导致卡顿问题,"

This reverts commit 56d45b94f5.

* Reapply "修改物体attach时,多次赋值当前时间导致卡顿问题,"

This reverts commit 07d9db20c3.

* 添加缺少物料:"plate_well_G12",

* host node新增resource add时间统计
create_resource新增handle
bump version to 0.9.2

* 修正物料上传时间
改用biomek_test
增加ResultInfoEncoder
支持返回结果上传

* 正确发送return_info结果

* 同步执行状态信息

* 取消raiseValueError提示

* Update biomek_test.py

* 0608 DONE

* 同步了Biomek.py 现在应可用

* biomek switch back to non-test

* temp disable initialize resource

* add

* fix tip resource data

* liquid states

* change to debug level

* Revert "change to debug level"

This reverts commit 5d9953c3e5.

* Reapply "change to debug level"

This reverts commit 2487bb6ffc.

* fix tip resource data

* add full device

* add moveit yaml

* 修复moveit
增加post_init阶段,给予ros_node反向

* remove necessary node

* fix moveit action client

* remove necessary imports

* Update moveit_interface.py

* fix handler_key uppercase

* json add liquids

* fix setup

* add

* change to "sources" and "targets" for lh

* bump version

* remove parent's parent link

* change arm's name

* change name

---------

Co-authored-by: Harvey Que <Q-Query@outlook.com>
Co-authored-by: zhangshixiang <@zhangshixiang>
Co-authored-by: q434343 <73513873+q434343@users.noreply.github.com>
Co-authored-by: Junhan Chang <changjh@pku.edu.cn>
Co-authored-by: Guangxin Zhang <guangxin.zhang.bio@gmail.com>
Co-authored-by: qxw138 <qxw@stu.pku.edu.cn>
2025-06-10 01:28:09 +08:00
q434343
133ffaac17 Device visualization (#39)
* Update README and MQTTClient for installation instructions and code improvements

* feat: 支持local_config启动
add: 增加对crt path的说明,为传入config.py的相对路径
move: web component

* add: registry description

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* feat: node_info_update srv
fix: OTDeck cant create

* close #12
feat: slave node registry

* feat: show machine name
fix: host node registry not uploaded

* feat: add hplc registry

* feat: add hplc registry

* fix: hplc status typo

* fix: devices/

* 完成启动OT并联动rviz

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* fix: device.class possible null

* fix: HPLC additions with online service

* fix: slave mode spin not working

* fix: slave mode spin not working

* 修复rviz位置问题,

修复rviz位置问题,
在无tf变动时减缓发送频率
在backend中添加物料跟随方法

* feat: 多ProtocolNode 允许子设备ID相同
feat: 上报发现的ActionClient
feat: Host重启动,通过discover机制要求slaveNode重新注册,实现信息及时上报

* feat: 支持env设置config

* fix: running logic

* fix: running logic

* fix: missing ot

* 在main中直接初始化republisher和物料的mesh节点

* 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中

* Device visualization (#14)

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* 修复rviz位置问题,

修复rviz位置问题,
在无tf变动时减缓发送频率
在backend中添加物料跟随方法

* fix: running logic

* fix: running logic

* fix: missing ot

* 在main中直接初始化republisher和物料的mesh节点

* 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中

---------

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

* fix: missing hostname in devices_names
fix: upload_file for model file

* fix: missing paho-mqtt package
bump version to 0.9.0

* fix startup
add ResourceCreateFromOuter.action

* fix type hint

* update actions

* update actions

* host node add_resource_from_outer
fix cmake list

* pass device config to device class

* add: bind_parent_ids to resource create action
fix: message convert string

* fix: host node should not be re_discovered

* feat: resource tracker support dict

* feat: add more necessary params

* feat: fix boolean null in registry action data

* feat: add outer resource

* 编写mesh添加action

* feat: append resource

* add action

* feat: vis 2d for plr

* fix

* fix: browser on rviz

* fix: cloud bridge error fallback to local

* fix: salve auto run rviz

* 初始化两个plate

* Device visualization (#22)

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* 修复rviz位置问题,

修复rviz位置问题,
在无tf变动时减缓发送频率
在backend中添加物料跟随方法

* fix: running logic

* fix: running logic

* fix: missing ot

* 在main中直接初始化republisher和物料的mesh节点

* 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中

* 编写mesh添加action

* add action

* fix

* fix: browser on rviz

* fix: cloud bridge error fallback to local

* fix: salve auto run rviz

* 初始化两个plate

---------

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

* fix: multi channel

* fix: aspirate

* fix: aspirate

* fix: aspirate

* fix: aspirate

* 提交

* fix: jobadd

* fix: jobadd

* fix: msg converter

* tijiao

* add resource creat easy action

* identify debug msg

* mq client id

* 提取lh的joint发布

* unify liquid_handler definition

* 修改物料跟随与物料添加逻辑

修改物料跟随与物料添加逻辑
将joint_publisher类移出lh的backends,但仍需要对lh的backends进行一些改写

* Revert "修改物料跟随与物料添加逻辑"

This reverts commit 498c997ad7.

* Reapply "修改物料跟随与物料添加逻辑"

This reverts commit 3a60d2ae81.

* Revert "Merge remote-tracking branch 'upstream/dev' into device_visualization"

This reverts commit fa727220af, reversing
changes made to 498c997ad7.

* 修改物料放下时的方法,如果选择

修改物料放下时的方法,
如果选择drop_trash,则删除物料显示
如果选择drop,则让其解除连接

* add biomek.py demo implementation

* 更新LiquidHandlerBiomek类,添加资源创建功能,优化协议创建方法,修复部分代码格式问题,更新YAML配置以支持新功能。

* Test

* fix biomek success type

* Convert LH action to biomek.

* Update biomek.py

* 注册表上报handle和schema (param input)

* 修复biomek缺少的字段

* delete 's'

* Remove warnings

* Update biomek.py

* Biomek test

* Update biomek.py

* 新增transfer_biomek的msg

* New transfer_biomek

* Updated transfer_biomek

* 更新transfer_biomek的msg

* 更新transfer_biomek的msg

* 支持Biomek创建

* new action

* fix key name typo

* New parameter for biomek to run.

* Refine

* Update

* new actions

* new actions

* 1

* registry

* fix biomek startup
add action handles

* fix handles not as default entry

* unilab添加moveit启动

1,整合所有moveit节点到一个move_group中,并整合所有的controller依次激活
2,添加pymoveit2的节点,使用json可直接启动
3,修改机械臂规划方式,添加约束,让冗余关节不会进行过多移动

* biomek_test.py

biomek_test.py是最新的版本,运行它会生成complete_biomek_protocol.json

* Update biomek.py

* biomek_test.py

* fix liquid_handler.biomek handles

* 修改物体attach时,多次赋值当前时间导致卡顿问题,

* Revert "修改物体attach时,多次赋值当前时间导致卡顿问题,"

This reverts commit 56d45b94f5.

* Reapply "修改物体attach时,多次赋值当前时间导致卡顿问题,"

This reverts commit 07d9db20c3.

* 添加缺少物料:"plate_well_G12",

* host node新增resource add时间统计
create_resource新增handle
bump version to 0.9.2

* 修正物料上传时间
改用biomek_test
增加ResultInfoEncoder
支持返回结果上传

* 正确发送return_info结果

* 同步执行状态信息

* 取消raiseValueError提示

* Update biomek_test.py

* 0608 DONE

* 同步了Biomek.py 现在应可用

* biomek switch back to non-test

* temp disable initialize resource

* add

* fix tip resource data

* liquid states

* change to debug level

* Revert "change to debug level"

This reverts commit 5d9953c3e5.

* Reapply "change to debug level"

This reverts commit 2487bb6ffc.

* fix tip resource data

* add full device

* add moveit yaml

* 修复moveit
增加post_init阶段,给予ros_node反向

* remove necessary node

* fix moveit action client

* remove necessary imports

* Update moveit_interface.py

* fix handler_key uppercase

* json add liquids

* fix setup

* add

* change to "sources" and "targets" for lh

* bump version

* remove parent's parent link

---------

Co-authored-by: Harvey Que <Q-Query@outlook.com>
Co-authored-by: wznln <18435084+Xuwznln@users.noreply.github.com>
Co-authored-by: zhangshixiang <@zhangshixiang>
Co-authored-by: Junhan Chang <changjh@pku.edu.cn>
Co-authored-by: Guangxin Zhang <guangxin.zhang.bio@gmail.com>
Co-authored-by: qxw138 <qxw@stu.pku.edu.cn>
2025-06-09 17:06:04 +08:00
Xuwznln
729a0fcf0c 37-biomek-i5i7 (#40)
* add biomek.py demo implementation

* 更新LiquidHandlerBiomek类,添加资源创建功能,优化协议创建方法,修复部分代码格式问题,更新YAML配置以支持新功能。

* Test

* fix biomek success type

* Convert LH action to biomek.

* Update biomek.py

* 注册表上报handle和schema (param input)

* 修复biomek缺少的字段

* delete 's'

* Remove warnings

* Update biomek.py

* Biomek test

* Update biomek.py

* 新增transfer_biomek的msg

* New transfer_biomek

* Updated transfer_biomek

* 更新transfer_biomek的msg

* 更新transfer_biomek的msg

* 支持Biomek创建

* new action

* fix key name typo

* New parameter for biomek to run.

* Refine

* Update

* new actions

* new actions

* 1

* registry

* fix biomek startup
add action handles

* fix handles not as default entry

* biomek_test.py

biomek_test.py是最新的版本,运行它会生成complete_biomek_protocol.json

* Update biomek.py

* biomek_test.py

* fix liquid_handler.biomek handles

* host node新增resource add时间统计
create_resource新增handle
bump version to 0.9.2

* 修正物料上传时间
改用biomek_test
增加ResultInfoEncoder
支持返回结果上传

* 正确发送return_info结果

* 同步执行状态信息

* 取消raiseValueError提示

* Update biomek_test.py

* 0608 DONE

* 同步了Biomek.py 现在应可用

* biomek switch back to non-test

* temp disable initialize resource

* Refine biomek

* Refine copy issue

* Refine

---------

Co-authored-by: Junhan Chang <changjh@pku.edu.cn>
Co-authored-by: Guangxin Zhang <guangxin.zhang.bio@gmail.com>
Co-authored-by: qxw138 <qxw@stu.pku.edu.cn>
2025-06-09 16:57:42 +08:00
Xuwznln
6ae77e0408 temp disable initialize resource 2025-06-08 17:07:48 +08:00
Xuwznln
bab4b1d67a biomek switch back to non-test 2025-06-08 17:05:48 +08:00
Guangxin Zhang
12c17ec26e 同步了Biomek.py 现在应可用 2025-06-08 16:58:19 +08:00
Guangxin Zhang
6577fe12eb 0608 DONE 2025-06-08 16:49:11 +08:00
qxw138
f1fee5fad9 Merge branch '37-biomek-i5i7' of https://github.com/dptech-corp/Uni-Lab-OS into 37-biomek-i5i7 2025-06-08 15:52:31 +08:00
qxw138
9b3377aedb Update biomek_test.py 2025-06-08 15:52:20 +08:00
Xuwznln
526327727d 取消raiseValueError提示 2025-06-08 15:34:56 +08:00
Xuwznln
aaa86314e3 同步执行状态信息 2025-06-08 15:34:16 +08:00
Xuwznln
6a14104e6b 正确发送return_info结果 2025-06-08 15:06:38 +08:00
Xuwznln
ab0c4b708b 修正物料上传时间
改用biomek_test
增加ResultInfoEncoder
支持返回结果上传
2025-06-08 14:43:07 +08:00
Xuwznln
c0b7f2decd host node新增resource add时间统计
create_resource新增handle
bump version to 0.9.2
2025-06-08 13:23:55 +08:00
Junhan Chang
b6c9530c61 Merge branch '37-biomek-i5i7' of https://github.com/dptech-corp/Uni-Lab-OS into 37-biomek-i5i7 2025-06-07 18:52:23 +08:00
Junhan Chang
8698821c52 fix liquid_handler.biomek handles 2025-06-07 18:52:20 +08:00
qxw138
3f53f88390 biomek_test.py 2025-06-07 15:21:20 +08:00
qxw138
e840516ba4 Update biomek.py 2025-06-06 22:50:11 +08:00
qxw138
146d8c5296 Merge branch '37-biomek-i5i7' of https://github.com/dptech-corp/Uni-Lab-OS into 37-biomek-i5i7 2025-06-06 22:49:35 +08:00
qxw138
6573c9e02e biomek_test.py
biomek_test.py是最新的版本,运行它会生成complete_biomek_protocol.json
2025-06-06 22:42:06 +08:00
Xuwznln
c7b9c6a825 fix handles not as default entry 2025-06-06 18:13:53 +08:00
Xuwznln
48c43d3303 fix biomek startup
add action handles
2025-06-06 17:45:54 +08:00
Xuwznln
55be5e8188 registry 2025-06-06 17:21:19 +08:00
qxw138
1b9f3c666d 1 2025-06-06 14:44:17 +08:00
qxw138
097114d38c new actions 2025-06-06 14:31:10 +08:00
qxw138
5bec899479 new actions 2025-06-06 13:56:39 +08:00
Guangxin Zhang
5e86112ebf Merge branch '37-biomek-i5i7' of https://github.com/dptech-corp/Uni-Lab-OS into 37-biomek-i5i7 2025-06-06 13:25:34 +08:00
Guangxin Zhang
24ecb13b79 Update 2025-06-06 13:22:15 +08:00
qxw138
2573d34713 Merge branch '37-biomek-i5i7' of https://github.com/dptech-corp/Uni-Lab-OS into 37-biomek-i5i7 2025-06-06 13:18:42 +08:00
Guangxin Zhang
106d71e1db Refine 2025-06-06 11:11:17 +08:00
Guangxin Zhang
3c2a4a64ac Merge branch '37-biomek-i5i7' of https://github.com/dptech-corp/Uni-Lab-OS into 37-biomek-i5i7 2025-06-06 11:11:10 +08:00
Guangxin Zhang
1e00a66a65 New parameter for biomek to run. 2025-06-06 11:05:36 +08:00
qxw138
46da42deef Merge branch '37-biomek-i5i7' of https://github.com/dptech-corp/Uni-Lab-OS into 37-biomek-i5i7 2025-06-06 00:13:11 +08:00
Xuwznln
101c1bc3cc fix key name typo 2025-06-05 22:15:57 +08:00
qxw138
a62112ae26 new action 2025-06-05 17:26:36 +08:00
Xuwznln
dd5a7cab75 支持Biomek创建 2025-06-05 16:04:44 +08:00
Xuwznln
39de3ac58e 更新transfer_biomek的msg 2025-06-05 15:41:16 +08:00
Xuwznln
b99969278c 更新transfer_biomek的msg 2025-06-05 15:30:51 +08:00
Guangxin Zhang
b957ad2f71 Merge branch '37-biomek-i5i7' of https://github.com/dptech-corp/Uni-Lab-OS into 37-biomek-i5i7 2025-06-04 21:49:27 +08:00
Guangxin Zhang
e1a7c3a103 Updated transfer_biomek 2025-06-04 21:49:22 +08:00
Guangxin Zhang
e63c15997c New transfer_biomek 2025-06-04 21:29:54 +08:00
Xuwznln
c5a495f409 新增transfer_biomek的msg 2025-06-04 19:03:00 +08:00
Guangxin Zhang
5b240cb0ea Update biomek.py 2025-06-04 17:30:53 +08:00
Guangxin Zhang
147b8f47c0 Biomek test 2025-06-04 16:38:18 +08:00
Guangxin Zhang
6d2489af5f Merge branch '37-biomek-i5i7' of https://github.com/dptech-corp/Uni-Lab-OS into 37-biomek-i5i7 2025-06-04 13:27:11 +08:00
Guangxin Zhang
807dcdd226 Update biomek.py 2025-06-04 13:27:05 +08:00
Guangxin Zhang
8a29bc5597 Remove warnings 2025-06-04 13:20:12 +08:00
Guangxin Zhang
6f6c70ee57 delete 's' 2025-06-04 13:11:45 +08:00
Xuwznln
478a85951c 修复biomek缺少的字段 2025-05-31 00:00:55 +08:00
Xuwznln
0f2555c90c 注册表上报handle和schema (param input) 2025-05-31 00:00:39 +08:00
Guangxin Zhang
d2dda6ee03 Merge branch '37-biomek-i5i7' of https://github.com/dptech-corp/Uni-Lab-OS into 37-biomek-i5i7 2025-05-30 17:11:23 +08:00
Guangxin Zhang
208540b307 Update biomek.py 2025-05-30 17:08:19 +08:00
Guangxin Zhang
cb7c56a1d9 Convert LH action to biomek. 2025-05-30 17:00:06 +08:00
Xuwznln
ea2e9c3e3a fix biomek success type 2025-05-30 16:50:13 +08:00
Guangxin Zhang
0452a68180 Test 2025-05-30 16:03:49 +08:00
Xuwznln
90a0f3db9b merge 2025-05-30 15:40:14 +08:00
Junhan Chang
055d120ba8 更新LiquidHandlerBiomek类,添加资源创建功能,优化协议创建方法,修复部分代码格式问题,更新YAML配置以支持新功能。 2025-05-30 15:38:23 +08:00
Junhan Chang
a948f09f60 add biomek.py demo implementation 2025-05-30 13:33:10 +08:00
Xuwznln
c2c2c2f020 Merge branch 'main' into dev
# Conflicts:
#	unilabos/registry/devices/liquid_handler.yaml
#	unilabos_msgs/action/ResourceCreateFromOuterEasy.action
2025-05-29 20:45:12 +08:00
wznln
4decd9a174 Merge branch '24-high-level-liquidhandler' into dev
# Conflicts:
#	unilabos/app/main.py
#	unilabos/registry/devices/liquid_handler.yaml
#	unilabos_msgs/CMakeLists.txt
2025-05-14 22:35:56 +08:00
Junhan Chang
83c765f0ab unify liquid_handler definition 2025-05-14 22:14:14 +08:00
wznln
3600b6f934 mq client id 2025-05-13 19:17:21 +08:00
wznln
f0576e5666 identify debug msg 2025-05-13 19:03:39 +08:00
wznln
8e1dbb56b1 add resource creat easy action 2025-05-13 18:36:02 +08:00
wznln
013c25f3aa Merge remote-tracking branch 'origin/dev' into fork/q434343/device_visualization 2025-05-07 04:10:25 +08:00
zhangshixiang
3d71c8bc78 Merge branch 'dev' into device_visualization 2025-05-07 04:05:12 +08:00
zhangshixiang
42f0994147 tijiao 2025-05-07 04:04:42 +08:00
wznln
4223f9b72c fix: msg converter 2025-05-07 04:04:02 +08:00
wznln
bec58e1301 fix: jobadd 2025-05-07 03:33:13 +08:00
wznln
6f9773157c fix: jobadd 2025-05-07 03:26:22 +08:00
zhangshixiang
da50e435c1 提交 2025-05-07 03:19:30 +08:00
wznln
34e03bbd6e fix: aspirate 2025-05-07 03:09:46 +08:00
zhangshixiang
ad5168c3eb Merge branch 'dev' into device_visualization 2025-05-07 03:06:41 +08:00
wznln
2dde5b6aae fix: aspirate 2025-05-07 03:02:35 +08:00
wznln
45a73e2f6d fix: aspirate 2025-05-07 02:51:56 +08:00
wznln
fbff27a52d fix: aspirate 2025-05-07 02:46:33 +08:00
zhangshixiang
1b190ee62f Merge remote-tracking branch 'upstream/dev' into device_visualization 2025-05-07 02:38:32 +08:00
wznln
83abf877b5 fix: multi channel 2025-05-07 02:36:53 +08:00
q434343
f3637d4043 Device visualization (#22)
* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* 修复rviz位置问题,

修复rviz位置问题,
在无tf变动时减缓发送频率
在backend中添加物料跟随方法

* fix: running logic

* fix: running logic

* fix: missing ot

* 在main中直接初始化republisher和物料的mesh节点

* 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中

* 编写mesh添加action

* add action

* fix

* fix: browser on rviz

* fix: cloud bridge error fallback to local

* fix: salve auto run rviz

* 初始化两个plate

---------

Co-authored-by: zhangshixiang <@zhangshixiang>
Co-authored-by: wznln <18435084+Xuwznln@users.noreply.github.com>
2025-05-07 02:12:29 +08:00
zhangshixiang
c12c2a876c 初始化两个plate 2025-05-07 02:06:01 +08:00
wznln
6cdd8c18e8 fix: salve auto run rviz 2025-05-07 00:45:37 +08:00
wznln
3d60cb36b8 fix: cloud bridge error fallback to local 2025-05-07 00:31:05 +08:00
wznln
5df304bc64 fix: browser on rviz 2025-05-07 00:11:29 +08:00
wznln
6d5ada06de Merge remote-tracking branch 'q434343/device_visualization' into device_visualization 2025-05-07 00:06:56 +08:00
zhangshixiang
aad23596b6 fix 2025-05-07 00:01:59 +08:00
wznln
b43f2321cd Merge remote-tracking branch 'origin/dev' into device_visualization 2025-05-06 23:58:56 +08:00
zhangshixiang
8617b1284f Merge remote-tracking branch 'upstream/dev' into device_visualization 2025-05-06 23:39:22 +08:00
wznln
cd1e9a9f7d feat: vis 2d for plr 2025-05-06 23:32:54 +08:00
zhangshixiang
3d607db49a add action 2025-05-06 23:27:42 +08:00
wznln
3dc62e3e99 feat: append resource 2025-05-06 23:13:29 +08:00
zhangshixiang
d199fda9a5 编写mesh添加action 2025-05-06 22:01:23 +08:00
wznln
ed2858a610 feat: add outer resource 2025-05-06 21:57:34 +08:00
wznln
de28c50d8b feat: fix boolean null in registry action data 2025-05-06 20:33:32 +08:00
wznln
e373220ce3 feat: add more necessary params 2025-05-06 20:18:49 +08:00
wznln
b6a3f17e9b feat: resource tracker support dict 2025-05-06 17:28:06 +08:00
wznln
49a9f05c51 fix: host node should not be re_discovered 2025-05-06 16:26:55 +08:00
wznln
32e370a562 add: bind_parent_ids to resource create action
fix: message convert string
2025-05-06 16:24:19 +08:00
wznln
852d10d751 pass device config to device class 2025-05-06 14:44:42 +08:00
wznln
b47f67d129 host node add_resource_from_outer
fix cmake list
2025-05-06 13:28:24 +08:00
wznln
194985222e update actions 2025-05-06 12:55:24 +08:00
wznln
948f590b47 update actions 2025-05-06 12:30:48 +08:00
wznln
164417e1cf Merge remote-tracking branch 'origin/main' into dev 2025-05-06 11:15:47 +08:00
wznln
1a107cfd18 fix type hint 2025-05-06 11:15:36 +08:00
wznln
65d0cbe28a Merge remote-tracking branch 'origin/main' into dev
# Conflicts:
#	unilabos/app/main.py
#	unilabos/ros/main_slave_run.py
2025-05-06 11:04:34 +08:00
wznln
3c98c77cab fix startup
add ResourceCreateFromOuter.action
2025-05-06 10:39:54 +08:00
wznln
d6b8104824 fix: missing paho-mqtt package
bump version to 0.9.0
2025-05-06 09:43:29 +08:00
wznln
1223e05dcc fix: missing hostname in devices_names
fix: upload_file for model file
2025-05-06 09:40:33 +08:00
q434343
a52133b7d0 Device visualization (#14)
* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* 修复rviz位置问题,

修复rviz位置问题,
在无tf变动时减缓发送频率
在backend中添加物料跟随方法

* fix: running logic

* fix: running logic

* fix: missing ot

* 在main中直接初始化republisher和物料的mesh节点

* 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中

---------

Co-authored-by: zhangshixiang <@zhangshixiang>
Co-authored-by: wznln <18435084+Xuwznln@users.noreply.github.com>
2025-05-06 09:37:57 +08:00
zhangshixiang
80380d1f4b 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中 2025-05-02 23:45:43 +08:00
zhangshixiang
5668310401 在main中直接初始化republisher和物料的mesh节点 2025-05-02 07:40:28 +08:00
wznln
78239ab1a3 fix: missing ot 2025-05-01 17:53:47 +08:00
wznln
fa5db06347 fix: running logic 2025-05-01 17:46:53 +08:00
wznln
2b428080e7 fix: running logic 2025-05-01 17:42:46 +08:00
wznln
9eb271f64e Merge remote-tracking branch 'origin/dev' into fork/q434343/device_visualization 2025-05-01 16:33:51 +08:00
wznln
752442cb37 feat: 支持env设置config 2025-05-01 14:52:50 +08:00
wznln
9d2bfec1dd feat: 多ProtocolNode 允许子设备ID相同
feat: 上报发现的ActionClient
feat: Host重启动,通过discover机制要求slaveNode重新注册,实现信息及时上报
2025-05-01 14:36:15 +08:00
zhangshixiang
5212d2d8eb 修复rviz位置问题,
修复rviz位置问题,
在无tf变动时减缓发送频率
在backend中添加物料跟随方法
2025-04-30 22:18:29 +08:00
zhangshixiang
44c191fe90 Merge branch 'device_visualization' of https://github.com/q434343/Uni-Lab-OS into device_visualization 2025-04-30 16:47:31 +08:00
wznln
7a51b2adc1 fix: slave mode spin not working 2025-04-30 15:48:33 +08:00
wznln
2d034f728a fix: slave mode spin not working 2025-04-30 15:21:29 +08:00
wznln
8ab108c489 fix: HPLC additions with online service 2025-04-30 11:53:10 +08:00
wznln
4dbb6649b4 fix: device.class possible null 2025-04-29 22:48:25 +08:00
zhangshixiang
dc197bffe8 完成启动OT并联动rviz 2025-04-29 22:15:39 +08:00
zhangshixiang
49bb11b2a3 使用json启动plr与3D模型仿真 2025-04-29 22:15:39 +08:00
zhangshixiang
d407423aaa 添加关节发布节点与物料可视化节点进入unilab 2025-04-29 22:15:39 +08:00
zhangshixiang
111c3f42e4 添加物料tf变化时,发送topic到前端
另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题
2025-04-29 22:15:02 +08:00
zhangshixiang
2990e70c25 修改模型方向,在yaml中添加变换属性 2025-04-29 22:15:02 +08:00
zhangshixiang
0d24606d46 完成TF发布 2025-04-29 22:15:02 +08:00
zhangshixiang
2baa232b86 完成在main中启动设备可视化
完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集
2025-04-29 22:15:02 +08:00
zhangshixiang
b7a16cdfc8 add 3d visualization 2025-04-29 22:14:18 +08:00
zhangshixiang
8921bcd9fb 完成启动OT并联动rviz 2025-04-29 22:13:34 +08:00
wznln
5038219fe6 fix: devices/ 2025-04-29 16:08:45 +08:00
wznln
0d2f1be37a fix: hplc status typo 2025-04-29 14:56:37 +08:00
wznln
6b649bfdec feat: add hplc registry 2025-04-29 14:52:02 +08:00
wznln
ba6a43c594 feat: add hplc registry 2025-04-29 14:50:33 +08:00
wznln
ea6f25d1ce feat: show machine name
fix: host node registry not uploaded
2025-04-29 14:39:14 +08:00
wznln
e5749a8058 close #12
feat: slave node registry
2025-04-29 13:42:30 +08:00
wznln
09fc17429e feat: node_info_update srv
fix: OTDeck cant create
2025-04-29 11:29:25 +08:00
zhangshixiang
bdf97be256 使用json启动plr与3D模型仿真 2025-04-29 10:08:34 +08:00
wznln
dbd1557095 Merge branch 'refs/heads/main' into dev 2025-04-29 10:04:56 +08:00
zhangshixiang
ff8b75bf1f 添加关节发布节点与物料可视化节点进入unilab 2025-04-27 19:07:39 +08:00
zhangshixiang
bed9720de3 添加物料tf变化时,发送topic到前端
另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题
2025-04-25 19:20:18 +08:00
zhangshixiang
1e01eae896 修改模型方向,在yaml中添加变换属性 2025-04-25 10:47:46 +08:00
zhangshixiang
6155ec2798 完成TF发布 2025-04-24 01:51:26 +08:00
zhangshixiang
279c5ed519 完成在main中启动设备可视化
完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集
2025-04-24 00:59:43 +08:00
zhangshixiang
5b4f580a6f add 3d visualization 2025-04-23 11:01:58 +08:00
wznln
e971424220 add: registry description 2025-04-20 18:21:35 +08:00
wznln
82881f5882 feat: 支持local_config启动
add: 增加对crt path的说明,为传入config.py的相对路径
move: web component
2025-04-20 18:11:35 +08:00
wznln
bb1cac0dbd Merge remote-tracking branch 'origin/main' into dev 2025-04-20 18:10:42 +08:00
Harvey Que
275e3a36f7 Update README and MQTTClient for installation instructions and code improvements 2025-04-18 15:32:49 +08:00
65 changed files with 3845 additions and 702 deletions

View File

@@ -0,0 +1,132 @@
name: Multi-Platform Conda Build
on:
push:
branches: [ main, dev ]
tags: [ 'v*' ]
pull_request:
branches: [ main, dev ]
workflow_dispatch:
inputs:
platforms:
description: '选择构建平台 (逗号分隔): linux-64, osx-64, osx-arm64, win-64'
required: false
default: 'osx-arm64'
jobs:
build:
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
platform: linux-64
env_file: unilabos-linux-64.yaml
- os: macos-13 # Intel
platform: osx-64
env_file: unilabos-osx-64.yaml
- os: macos-latest # ARM64
platform: osx-arm64
env_file: unilabos-osx-arm64.yaml
- os: windows-latest
platform: win-64
env_file: unilabos-win64.yaml
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash -l {0}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check if platform should be built
id: should_build
run: |
if [[ "${{ github.event_name }}" != "workflow_dispatch" ]]; then
echo "should_build=true" >> $GITHUB_OUTPUT
elif [[ -z "${{ github.event.inputs.platforms }}" ]]; then
echo "should_build=true" >> $GITHUB_OUTPUT
elif [[ "${{ github.event.inputs.platforms }}" == *"${{ matrix.platform }}"* ]]; then
echo "should_build=true" >> $GITHUB_OUTPUT
else
echo "should_build=false" >> $GITHUB_OUTPUT
fi
- name: Setup Miniconda
if: steps.should_build.outputs.should_build == 'true'
uses: conda-incubator/setup-miniconda@v3
with:
miniconda-version: "latest"
channels: conda-forge,robostack-staging,defaults
channel-priority: strict
activate-environment: build-env
auto-activate-base: false
auto-update-conda: false
show-channel-urls: true
- name: Install boa and build tools
if: steps.should_build.outputs.should_build == 'true'
run: |
conda install -c conda-forge boa conda-build
- name: Show environment info
if: steps.should_build.outputs.should_build == 'true'
run: |
conda info
conda list | grep -E "(boa|conda-build)"
echo "Platform: ${{ matrix.platform }}"
echo "OS: ${{ matrix.os }}"
- name: Build conda package
if: steps.should_build.outputs.should_build == 'true'
run: |
if [[ "${{ matrix.platform }}" == "osx-arm64" ]]; then
boa build -m ./recipes/conda_build_config.yaml -m ./recipes/macos_sdk_config.yaml ./recipes/ros-humble-unilabos-msgs
else
boa build -m ./recipes/conda_build_config.yaml ./recipes/ros-humble-unilabos-msgs
fi
- name: List built packages
if: steps.should_build.outputs.should_build == 'true'
run: |
echo "Built packages in conda-bld:"
find $CONDA_PREFIX/conda-bld -name "*.tar.bz2" | head -10
ls -la $CONDA_PREFIX/conda-bld/${{ matrix.platform }}/ || echo "${{ matrix.platform }} directory not found"
ls -la $CONDA_PREFIX/conda-bld/noarch/ || echo "noarch directory not found"
echo "CONDA_PREFIX: $CONDA_PREFIX"
echo "Full path would be: $CONDA_PREFIX/conda-bld/**/*.tar.bz2"
- name: Prepare artifacts for upload
if: steps.should_build.outputs.should_build == 'true'
run: |
mkdir -p ${{ runner.temp }}/conda-packages
find $CONDA_PREFIX/conda-bld -name "*.tar.bz2" -exec cp {} ${{ runner.temp }}/conda-packages/ \;
echo "Copied files to temp directory:"
ls -la ${{ runner.temp }}/conda-packages/
- name: Upload conda package artifacts
if: steps.should_build.outputs.should_build == 'true'
uses: actions/upload-artifact@v4
with:
name: conda-package-${{ matrix.platform }}
path: ${{ runner.temp }}/conda-packages
if-no-files-found: warn
retention-days: 30
- name: Create release assets (on tags)
if: steps.should_build.outputs.should_build == 'true' && startsWith(github.ref, 'refs/tags/')
run: |
mkdir -p release-assets
find $CONDA_PREFIX/conda-bld -name "*.tar.bz2" -exec cp {} release-assets/ \;
- name: Upload to release
if: steps.should_build.outputs.should_build == 'true' && startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v1
with:
files: release-assets/*
draft: false
prerelease: false

4
.gitignore vendored
View File

@@ -234,3 +234,7 @@ CATKIN_IGNORE
*.graphml
unilabos/device_mesh/view_robot.rviz
# Certs
**/.certs

View File

@@ -1,3 +1,5 @@
recursive-include unilabos/registry *.yaml
recursive-include unilabos/app/web *.html
recursive-include unilabos/app/web *.css
recursive-include unilabos/device_mesh/devices *
recursive-include unilabos/device_mesh/resources *

View File

@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n environment_name
# Currently, you need to install the `unilabos_msgs` package
# You can download the system-specific package from the Release page
conda install ros-humble-unilabos-msgs-0.9.4-xxxxx.tar.bz2
conda install ros-humble-unilabos-msgs-0.9.6-xxxxx.tar.bz2
# Install PyLabRobot and other prerequisites
git clone https://github.com/PyLabRobot/pylabrobot plr_repo

View File

@@ -49,7 +49,7 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名
# 现阶段,需要安装 `unilabos_msgs` 包
# 可以前往 Release 页面下载系统对应的包进行安装
conda install ros-humble-unilabos-msgs-0.9.4-xxxxx.tar.bz2
conda install ros-humble-unilabos-msgs-0.9.6-xxxxx.tar.bz2
# 安装PyLabRobot等前置
git clone https://github.com/PyLabRobot/pylabrobot plr_repo

View File

@@ -0,0 +1,7 @@
CONDA_BUILD_SYSROOT:
- /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk
MACOSX_DEPLOYMENT_TARGET:
- "11.0"
CONDA_SUBDIR:
- osx-arm64
# boa build -m ./recipes/conda_build_config.yaml -m ./recipes/macos_sdk_config.yaml ./recipes/ros-humble-unilabos-msgs

View File

@@ -1,6 +1,6 @@
package:
name: ros-humble-unilabos-msgs
version: 0.9.4
version: 0.9.6
source:
path: ../../unilabos_msgs
folder: ros-humble-unilabos-msgs/src/work
@@ -50,12 +50,12 @@ requirements:
- robostack-staging::ros-humble-rosidl-default-generators
- robostack-staging::ros-humble-std-msgs
- robostack-staging::ros-humble-geometry-msgs
- robostack-staging::ros2-distro-mutex=0.6.*
- robostack-staging::ros2-distro-mutex=0.5.*
run:
- robostack-staging::ros-humble-action-msgs
- robostack-staging::ros-humble-ros-workspace
- robostack-staging::ros-humble-rosidl-default-runtime
- robostack-staging::ros-humble-std-msgs
- robostack-staging::ros-humble-geometry-msgs
- robostack-staging::ros2-distro-mutex=0.6.*
# - robostack-staging::ros2-distro-mutex=0.6.*
- sel(osx and x86_64): __osx >={{ MACOSX_DEPLOYMENT_TARGET|default('10.14') }}

View File

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

View File

@@ -4,7 +4,7 @@ package_name = 'unilabos'
setup(
name=package_name,
version='0.9.4',
version='0.9.6',
packages=find_packages(),
include_package_data=True,
install_requires=['setuptools'],
@@ -17,6 +17,7 @@ setup(
entry_points={
'console_scripts': [
"unilab = unilabos.app.main:main",
"unilab-register = unilabos.app.register:main"
],
},
)

View File

@@ -2,4 +2,10 @@
```bash
ros2 action send_goal /devices/host_node/create_resource_detailed unilabos_msgs/action/_resource_create_from_outer/ResourceCreateFromOuter "{ resources: [ { 'category': '', 'children': [], 'config': { 'type': 'Well', 'size_x': 6.86, 'size_y': 6.86, 'size_z': 10.67, 'rotation': { 'x': 0, 'y': 0, 'z': 0, 'type': 'Rotation' }, 'category': 'well', 'model': null, 'max_volume': 360, 'material_z_thickness': 0.5, 'compute_volume_from_height': null, 'compute_height_from_volume': null, 'bottom_type': 'flat', 'cross_section_type': 'circle' }, 'data': { 'liquids': [], 'pending_liquids': [], 'liquid_history': [] }, 'id': 'plate_well_11_7', 'name': 'plate_well_11_7', 'pose': { 'orientation': { 'w': 1.0, 'x': 0.0, 'y': 0.0, 'z': 0.0 }, 'position': { 'x': 0.0, 'y': 0.0, 'z': 0.0 } }, 'sample_id': '', 'parent': 'plate', 'type': 'device' } ], device_ids: [ 'PLR_STATION' ], bind_parent_ids: [ 'plate' ], bind_locations: [ { 'x': 0.0, 'y': 0.0, 'z': 0.0 } ], other_calling_params: [ '{}' ] }"
```
使用mock_all.json启动重新捕获MockContainerForChiller1
```bash
ros2 action send_goal /devices/host_node/create_resource unilabos_msgs/action/_resource_create_from_outer_easy/ResourceCreateFromOuterEasy "{ 'res_id': 'MockContainerForChiller1', 'device_id': 'MockChiller1', 'class_name': 'container', 'parent': 'MockChiller1', 'bind_locations': { 'x': 0.0, 'y': 0.0, 'z': 0.0 }, 'liquid_input_slot': [ -1 ], 'liquid_type': [ 'CuCl2' ], 'liquid_volume': [ 100.0 ], 'slot_on_deck': '' }"
```

View File

@@ -0,0 +1,32 @@
1. 用到的仪器
virtual_multiway_valve() 八通阀门
virtual_transfer_pump() 转移泵
virtual_centrifuge() 离心机
virtual_rotavap() 旋蒸仪
virtual_heatchill() 加热器
virtual_stirrer() 搅拌器
virtual_solenoid_valve() 电磁阀
virtual_vacuum_pump(√) vacuum_pump.mock 真空泵
virtual_gas_source(√) 气源
virtual_filter() 过滤器
virtual_column(√) 层析柱
separator() homemade_grbl_conductivity 分液漏斗
2. 用到的protocol
AddProtocol()
TransferProtocol() 应该用pump_protocol.py删掉transfer
StartStirProtocol()
StopStirProtocol()
StirProtocol()
RunColumnProtocol()
CentrifugeProtocol()
FilterProtocol()
CleanVesselProtocol()
DissolveProtocol()
FilterThroughProtocol()
WashSolidProtocol()
SeparateProtocol()
EvaporateProtocol()
HeatChillProtocol()
HeatChillStartProtocol()
HeatChillStopProtocol()
EvacuateAndRefillProtocol()

View File

@@ -0,0 +1,887 @@
{
"nodes": [
{
"id": "ComprehensiveProtocolStation",
"name": "综合协议测试工作站",
"children": [
"multiway_valve_1",
"multiway_valve_2",
"transfer_pump_1",
"transfer_pump_2",
"reagent_bottle_1",
"reagent_bottle_2",
"reagent_bottle_3",
"reagent_bottle_4",
"reagent_bottle_5",
"centrifuge_1",
"rotavap_1",
"main_reactor",
"heater_1",
"stirrer_1",
"stirrer_2",
"waste_bottle_1",
"waste_bottle_2",
"solenoid_valve_1",
"solenoid_valve_2",
"vacuum_pump_1",
"gas_source_1",
"filter_1",
"column_1",
"separator_1",
"collection_bottle_1",
"collection_bottle_2",
"collection_bottle_3"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 600,
"y": 400,
"z": 0
},
"config": {
"protocol_type": [
"AddProtocol",
"TransferProtocol",
"StartStirProtocol",
"StopStirProtocol",
"StirProtocol",
"RunColumnProtocol",
"CentrifugeProtocol",
"FilterProtocol",
"CleanVesselProtocol",
"DissolveProtocol",
"FilterThroughProtocol",
"WashSolidProtocol",
"SeparateProtocol",
"EvaporateProtocol",
"HeatChillProtocol",
"HeatChillStartProtocol",
"HeatChillStopProtocol",
"EvacuateAndRefillProtocol",
"PumpTransferProtocol"
]
},
"data": {}
},
{
"id": "multiway_valve_1",
"name": "八通阀门1",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 400,
"y": 300,
"z": 0
},
"config": {
"positions": 8
},
"data": {
"valve_state": "Ready",
"current_position": 1
}
},
{
"id": "multiway_valve_2",
"name": "八通阀门2",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 800,
"y": 300,
"z": 0
},
"config": {
"positions": 8
},
"data": {
"valve_state": "Ready",
"current_position": 1
}
},
{
"id": "transfer_pump_1",
"name": "转移泵1",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 350,
"y": 250,
"z": 0
},
"config": {
"max_volume": 25.0,
"transfer_rate": 10.0
},
"data": {
"status": "Idle",
"current_volume": 0.0
}
},
{
"id": "transfer_pump_2",
"name": "转移泵2",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 850,
"y": 250,
"z": 0
},
"config": {
"max_volume": 25.0,
"transfer_rate": 10.0
},
"data": {
"status": "Idle",
"current_volume": 0.0
}
},
{
"id": "reagent_bottle_1",
"name": "试剂瓶1-DMF",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "container",
"class": "container",
"position": {
"x": 200,
"y": 150,
"z": 0
},
"config": {
"volume": 1000.0,
"reagent": "DMF"
},
"data": {
"current_volume": 1000.0,
"reagent_name": "DMF"
}
},
{
"id": "reagent_bottle_2",
"name": "试剂瓶2-乙酸乙酯",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "container",
"class": "container",
"position": {
"x": 250,
"y": 150,
"z": 0
},
"config": {
"volume": 1000.0,
"reagent": "ethyl_acetate"
},
"data": {
"current_volume": 1000.0,
"reagent_name": "ethyl_acetate"
}
},
{
"id": "reagent_bottle_3",
"name": "试剂瓶3-己烷",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "container",
"class": "container",
"position": {
"x": 300,
"y": 150,
"z": 0
},
"config": {
"volume": 1000.0,
"reagent": "hexane"
},
"data": {
"current_volume": 1000.0,
"reagent_name": "hexane"
}
},
{
"id": "reagent_bottle_4",
"name": "试剂瓶4-甲醇",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "container",
"class": "container",
"position": {
"x": 900,
"y": 150,
"z": 0
},
"config": {
"volume": 1000.0,
"reagent": "methanol"
},
"data": {
"current_volume": 1000.0,
"reagent_name": "methanol"
}
},
{
"id": "reagent_bottle_5",
"name": "试剂瓶5-水",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "container",
"class": "container",
"position": {
"x": 950,
"y": 150,
"z": 0
},
"config": {
"volume": 1000.0,
"reagent": "water"
},
"data": {
"current_volume": 1000.0,
"reagent_name": "water"
}
},
{
"id": "centrifuge_1",
"name": "离心机",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "device",
"class": "virtual_centrifuge",
"position": {
"x": 200,
"y": 400,
"z": 0
},
"config": {
"max_speed": 15000.0,
"max_temp": 40.0,
"min_temp": 4.0
},
"data": {
"current_speed": 0.0,
"status": "Idle"
}
},
{
"id": "rotavap_1",
"name": "旋转蒸发仪",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "device",
"class": "virtual_rotavap",
"position": {
"x": 300,
"y": 400,
"z": 0
},
"config": {
"max_temp": 180.0,
"max_rotation_speed": 280.0
},
"data": {
"status": "Idle",
"current_temp": 25.0,
"rotation_speed": 0.0
}
},
{
"id": "main_reactor",
"name": "主反应器",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "container",
"class": "container",
"position": {
"x": 400,
"y": 450,
"z": 0
},
"config": {
"volume": 500.0,
"max_temp": 200.0,
"min_temp": -20.0,
"has_stirrer": true,
"has_heater": true
},
"data": {
"current_volume": 0.0,
"current_temp": 25.0
}
},
{
"id": "heater_1",
"name": "加热器",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "device",
"class": "virtual_heatchill",
"position": {
"x": 450,
"y": 450,
"z": 0
},
"config": {
"max_temp": 200.0,
"min_temp": -20.0
},
"data": {
"status": "Idle",
"current_temp": 25.0
}
},
{
"id": "stirrer_1",
"name": "搅拌器1",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 350,
"y": 450,
"z": 0
},
"config": {
"max_speed": 2000.0
},
"data": {
"status": "Idle",
"current_speed": 0.0
}
},
{
"id": "stirrer_2",
"name": "搅拌器2",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 351,
"y": 451,
"z": 0
},
"config": {
"max_speed": 2000.0
},
"data": {
"status": "Idle",
"current_speed": 0.0
}
},
{
"id": "waste_bottle_1",
"name": "废液瓶1",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "container",
"class": "container",
"position": {
"x": 500,
"y": 400,
"z": 0
},
"config": {
"volume": 2000.0
},
"data": {
"current_volume": 0.0
}
},
{
"id": "waste_bottle_2",
"name": "废液瓶2",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "container",
"class": "container",
"position": {
"x": 1100,
"y": 500,
"z": 0
},
"config": {
"volume": 2000.0
},
"data": {
"current_volume": 0.0
}
},
{
"id": "solenoid_valve_1",
"name": "电磁阀1",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "device",
"class": "virtual_solenoid_valve",
"position": {
"x": 700,
"y": 200,
"z": 0
},
"config": {
"voltage": 12.0,
"response_time": 0.1
},
"data": {
"valve_state": "Closed",
"is_open": false
}
},
{
"id": "solenoid_valve_2",
"name": "电磁阀2",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "device",
"class": "virtual_solenoid_valve",
"position": {
"x": 700,
"y": 150,
"z": 0
},
"config": {
"voltage": 12.0,
"response_time": 0.1
},
"data": {
"valve_state": "Closed",
"is_open": false
}
},
{
"id": "vacuum_pump_1",
"name": "真空泵",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "device",
"class": "virtual_vacuum_pump",
"position": {
"x": 650,
"y": 200,
"z": 0
},
"config": {
"max_vacuum": 0.1,
"pump_rate": 50.0
},
"data": {
"status": "Off",
"current_vacuum": 1.0
}
},
{
"id": "gas_source_1",
"name": "气源",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "device",
"class": "virtual_gas_source",
"position": {
"x": 650,
"y": 150,
"z": 0
},
"config": {},
"data": {
"gas_type": "nitrogen",
"max_pressure": 5.0
}
},
{
"id": "filter_1",
"name": "过滤器",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "device",
"class": "virtual_filter",
"position": {
"x": 900,
"y": 400,
"z": 0
},
"config": {
"filter_type": "membrane",
"max_pressure": 5.0
},
"data": {
"status": "Ready",
"pressure": 0.0
}
},
{
"id": "column_1",
"name": "洗脱柱",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "device",
"class": "virtual_column",
"position": {
"x": 950,
"y": 400,
"z": 0
},
"config": {
"column_type": "silica_gel",
"length": 30.0,
"diameter": 2.5
},
"data": {
"status": "Ready",
"loaded": false
}
},
{
"id": "separator_1",
"name": "分液器",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "device",
"class": "virtual_separator",
"position": {
"x": 1000,
"y": 450,
"z": 0
},
"config": {
"volume": 250.0,
"has_phases": true
},
"data": {
"status": "Ready",
"phase_separation": false
}
},
{
"id": "collection_bottle_1",
"name": "接收瓶1",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "container",
"class": "container",
"position": {
"x": 900,
"y": 500,
"z": 0
},
"config": {
"volume": 250.0
},
"data": {
"current_volume": 0.0
}
},
{
"id": "collection_bottle_2",
"name": "接收瓶2",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "container",
"class": "container",
"position": {
"x": 950,
"y": 500,
"z": 0
},
"config": {
"volume": 250.0
},
"data": {
"current_volume": 0.0
}
},
{
"id": "collection_bottle_3",
"name": "接收瓶3",
"children": [],
"parent": "ComprehensiveProtocolStation",
"type": "container",
"class": "container",
"position": {
"x": 1050,
"y": 500,
"z": 0
},
"config": {
"volume": 250.0
},
"data": {
"current_volume": 0.0
}
}
],
"links": [
{
"id": "link_valve1_pump1",
"source": "multiway_valve_1",
"target": "transfer_pump_1",
"type": "fluid",
"port": {
"multiway_valve_1": "transferpump",
"transfer_pump_1": "transferpump"
}
},
{
"id": "link_valve1_reagent1",
"source": "multiway_valve_1",
"target": "reagent_bottle_1",
"type": "fluid",
"port": {
"multiway_valve_1": "1",
"reagent_bottle_1": "top"
}
},
{
"id": "link_valve1_reagent2",
"source": "multiway_valve_1",
"target": "reagent_bottle_2",
"type": "fluid",
"port": {
"multiway_valve_1": "2",
"reagent_bottle_2": "top"
}
},
{
"id": "link_valve1_reagent3",
"source": "multiway_valve_1",
"target": "reagent_bottle_3",
"type": "fluid",
"port": {
"multiway_valve_1": "3",
"reagent_bottle_3": "top"
}
},
{
"id": "link_valve1_centrifuge",
"source": "multiway_valve_1",
"target": "centrifuge_1",
"type": "fluid",
"port": {
"multiway_valve_1": "4",
"centrifuge_1": "centrifuge"
}
},
{
"id": "link_valve1_rotavap",
"source": "multiway_valve_1",
"target": "rotavap_1",
"type": "fluid",
"port": {
"multiway_valve_1": "5",
"rotavap_1": "rotavap-sample"
}
},
{
"id": "link_valve1_reactor",
"source": "multiway_valve_1",
"target": "main_reactor",
"type": "fluid",
"port": {
"multiway_valve_1": "6",
"main_reactor": "top"
}
},
{
"id": "link_valve1_waste1",
"source": "multiway_valve_1",
"target": "waste_bottle_1",
"type": "fluid",
"port": {
"multiway_valve_1": "7",
"waste_bottle_1": "top"
}
},
{
"id": "link_valve1_valve2",
"source": "multiway_valve_1",
"target": "multiway_valve_2",
"type": "fluid",
"port": {
"multiway_valve_1": "8",
"multiway_valve_2": "1"
}
},
{
"id": "link_valve2_pump2",
"source": "multiway_valve_2",
"target": "transfer_pump_2",
"type": "fluid",
"port": {
"multiway_valve_2": "transferpump",
"transfer_pump_2": "transferpump"
}
},
{
"id": "link_valve2_solenoid1",
"source": "multiway_valve_2",
"target": "solenoid_valve_1",
"type": "fluid",
"port": {
"multiway_valve_2": "2",
"solenoid_valve_1": "in"
}
},
{
"id": "link_solenoid1_vacuum",
"source": "solenoid_valve_1",
"target": "vacuum_pump_1",
"type": "fluid",
"port": {
"solenoid_valve_1": "out",
"vacuum_pump_1": "vacuumpump"
}
},
{
"id": "link_valve2_solenoid2",
"source": "multiway_valve_2",
"target": "solenoid_valve_2",
"type": "fluid",
"port": {
"multiway_valve_2": "3",
"solenoid_valve_2": "in"
}
},
{
"id": "link_solenoid2_gas",
"source": "solenoid_valve_2",
"target": "gas_source_1",
"type": "fluid",
"port": {
"solenoid_valve_2": "out",
"gas_source_1": "gassource"
}
},
{
"id": "link_valve2_filter",
"source": "multiway_valve_2",
"target": "filter_1",
"type": "fluid",
"port": {
"multiway_valve_2": "4",
"filter_1": "filterin"
}
},
{
"id": "link_filter_collection1",
"source": "filter_1",
"target": "collection_bottle_1",
"type": "fluid",
"port": {
"filter_1": "filtrate_out",
"collection_bottle_1": "top"
}
},
{
"id": "link_valve2_column",
"source": "multiway_valve_2",
"target": "column_1",
"type": "fluid",
"port": {
"multiway_valve_2": "5",
"column_1": "columnin"
}
},
{
"id": "link_column_collection2",
"source": "column_1",
"target": "collection_bottle_2",
"type": "fluid",
"port": {
"column_1": "columnout",
"collection_bottle_2": "top"
}
},
{
"id": "link_valve2_separator",
"source": "multiway_valve_2",
"target": "separator_1",
"type": "fluid",
"port": {
"multiway_valve_2": "6",
"separator_1": "separatorin"
}
},
{
"id": "link_separator_collection3",
"source": "separator_1",
"target": "collection_bottle_3",
"type": "fluid",
"port": {
"separator_1": "separatorout",
"collection_bottle_3": "top"
}
},
{
"id": "link_separator_stirrer_2",
"source": "separator_1",
"target": "stirrer_2",
"type": "fluid",
"port": {
"separator_1": "separatorout",
"stirrer_2": "stirrer"
}
},
{
"id": "link_separator_waste2",
"source": "separator_1",
"target": "waste_bottle_2",
"type": "fluid",
"port": {
"separator_1": "separatorout",
"waste_bottle_2": "top"
}
},
{
"id": "link_valve2_reagent4",
"source": "multiway_valve_2",
"target": "reagent_bottle_4",
"type": "fluid",
"port": {
"multiway_valve_2": "7",
"reagent_bottle_4": "top"
}
},
{
"id": "link_valve2_reagent5",
"source": "multiway_valve_2",
"target": "reagent_bottle_5",
"type": "fluid",
"port": {
"multiway_valve_2": "8",
"reagent_bottle_5": "top"
}
},
{
"id": "mech_stirrer_reactor",
"source": "stirrer_1",
"target": "main_reactor",
"type": "fluid",
"port": {
"stirrer_1": "stirrer",
"main_reactor": "top"
}
},
{
"id": "thermal_heater_reactor",
"source": "heater_1",
"target": "main_reactor",
"type": "fluid",
"port": {
"heater_1": "heatchill",
"main_reactor": "bottom"
}
}
]
}

View File

@@ -3,7 +3,9 @@
{
"id": "MockChiller1",
"name": "模拟冷却器",
"children": [],
"children": [
"MockContainerForChiller1"
],
"parent": null,
"type": "device",
"class": "mock_chiller",
@@ -25,6 +27,22 @@
"purpose": ""
}
},
{
"id": "MockContainerForChiller1",
"name": "模拟容器",
"type": "container",
"parent": "MockChiller1",
"position": {
"x": 5,
"y": 0,
"z": 0
},
"data": {
"liquid_type": "CuCl2",
"liquid_volume": "100"
},
"children": []
},
{
"id": "MockFilter1",
"name": "模拟过滤器",

View File

@@ -4,58 +4,83 @@
"id": "AddTestStation",
"name": "添加试剂测试工作站",
"children": [
"pump_add",
"flask_1",
"flask_2",
"flask_3",
"flask_4",
"reactor",
"transfer_pump",
"multiway_valve",
"stirrer",
"flask_air"
"flask_reagent1",
"flask_reagent2",
"flask_reagent3",
"flask_reagent4",
"reactor",
"flask_waste",
"flask_rinsing",
"flask_buffer"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"x": 620,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol"]
"protocol_type": ["AddProtocol", "TransferProtocol", "StartStirProtocol", "StopStirProtocol"]
},
"data": {}
},
{
"id": "pump_add",
"name": "pump_add",
"id": "transfer_pump",
"name": "注射器泵",
"children": [],
"parent": "AddTestStation",
"type": "device",
"class": "virtual_pump",
"class": "virtual_transfer_pump",
"position": {
"x": 520.6111111111111,
"x": 520,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 25.0
"max_volume": 50.0,
"transfer_rate": 5.0
},
"data": {
"status": "Idle"
}
},
{
"id": "multiway_valve",
"name": "八通阀门",
"children": [],
"parent": "AddTestStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 420,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"positions": 8
},
"data": {
"status": "Idle",
"current_position": 1
}
},
{
"id": "stirrer",
"name": "stirrer",
"name": "搅拌器",
"children": [],
"parent": "AddTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 698.1111111111111,
"y": 478,
"x": 720,
"y": 450,
"z": 0
},
"config": {
@@ -68,110 +93,115 @@
}
},
{
"id": "flask_1",
"name": "通用试剂瓶1",
"id": "flask_reagent1",
"name": "试剂瓶1 (甲醇)",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"y": 400,
"z": 0
},
"config": {
"max_volume": 2000.0
"max_volume": 1000.0
},
"data": {
"liquid": []
"liquid": [
{
"name": "甲醇",
"volume": 800.0,
"concentration": "99.9%"
}
]
}
},
{
"id": "flask_2",
"name": "通用试剂瓶2",
"id": "flask_reagent2",
"name": "试剂瓶2 (乙醇)",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"x": 180,
"y": 400,
"z": 0
},
"config": {
"max_volume": 2000.0
"max_volume": 1000.0
},
"data": {
"liquid": []
"liquid": [
{
"name": "乙醇",
"volume": 750.0,
"concentration": "95%"
}
]
}
},
{
"id": "flask_3",
"name": "通用试剂瓶3",
"id": "flask_reagent3",
"name": "试剂瓶3 (丙酮)",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"x": 260,
"y": 400,
"z": 0
},
"config": {
"max_volume": 2000.0
"max_volume": 1000.0
},
"data": {
"liquid": []
"liquid": [
{
"name": "丙酮",
"volume": 900.0,
"concentration": "99.5%"
}
]
}
},
{
"id": "flask_4",
"name": "通用试剂瓶4",
"id": "flask_reagent4",
"name": "试剂瓶4 (二氯甲烷)",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 550,
"y": 428,
"x": 340,
"y": 400,
"z": 0
},
"config": {
"max_volume": 2000.0
"max_volume": 1000.0
},
"data": {
"liquid": []
"liquid": [
{
"name": "二氯甲烷",
"volume": 850.0,
"concentration": "99.8%"
}
]
}
},
{
"id": "reactor",
"name": "reactor",
"name": "反应器",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 5000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_air",
"name": "flask_air",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 800,
"y": 300,
"x": 720,
"y": 400,
"z": 0
},
"config": {
@@ -180,70 +210,166 @@
"data": {
"liquid": []
}
},
{
"id": "flask_waste",
"name": "废液瓶",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 850,
"y": 400,
"z": 0
},
"config": {
"max_volume": 3000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_rinsing",
"name": "冲洗液瓶",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 950,
"y": 300,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "去离子水",
"volume": 800.0,
"concentration": "纯净"
}
]
}
},
{
"id": "flask_buffer",
"name": "缓冲液瓶",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 950,
"y": 400,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "磷酸盐缓冲液",
"volume": 700.0,
"concentration": "0.1M, pH 7.4"
}
]
}
}
],
"links": [
{
"source": "stirrer",
"source": "transfer_pump",
"target": "multiway_valve",
"type": "physical",
"port": {
"transfer_pump": "syringe-port",
"multiway_valve": "multiway-valve-inlet"
}
},
{
"source": "multiway_valve",
"target": "flask_reagent1",
"type": "physical",
"port": {
"multiway_valve": "multiway-valve-port-1",
"flask_reagent1": "top"
}
},
{
"source": "multiway_valve",
"target": "flask_reagent2",
"type": "physical",
"port": {
"multiway_valve": "multiway-valve-port-2",
"flask_reagent2": "top"
}
},
{
"source": "multiway_valve",
"target": "flask_reagent3",
"type": "physical",
"port": {
"multiway_valve": "multiway-valve-port-3",
"flask_reagent3": "top"
}
},
{
"source": "multiway_valve",
"target": "flask_reagent4",
"type": "physical",
"port": {
"multiway_valve": "multiway-valve-port-4",
"flask_reagent4": "top"
}
},
{
"source": "multiway_valve",
"target": "reactor",
"type": "physical",
"port": {
"stirrer": "top",
"reactor": "bottom"
}
},
{
"source": "pump_add",
"target": "flask_1",
"type": "physical",
"port": {
"pump_add": "outlet",
"flask_1": "top"
}
},
{
"source": "pump_add",
"target": "flask_2",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_2": "top"
}
},
{
"source": "pump_add",
"target": "flask_3",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_3": "top"
}
},
{
"source": "pump_add",
"target": "flask_4",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_4": "top"
}
},
{
"source": "pump_add",
"target": "reactor",
"type": "physical",
"port": {
"pump_add": "outlet",
"multiway_valve": "multiway-valve-port-5",
"reactor": "top"
}
},
{
"source": "pump_add",
"target": "flask_air",
"source": "multiway_valve",
"target": "flask_waste",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_air": "top"
"multiway_valve": "multiway-valve-port-6",
"flask_waste": "top"
}
},
{
"source": "multiway_valve",
"target": "flask_rinsing",
"type": "physical",
"port": {
"multiway_valve": "multiway-valve-port-7",
"flask_rinsing": "top"
}
},
{
"source": "multiway_valve",
"target": "flask_buffer",
"type": "physical",
"port": {
"multiway_valve": "multiway-valve-port-8",
"flask_buffer": "top"
}
},
{
"source": "stirrer",
"target": "reactor",
"type": "physical",
"port": {
"stirrer": "stirrer-vessel",
"reactor": "bottom"
}
}
]

View File

@@ -30,14 +30,17 @@
"children": [],
"parent": "ReactorX",
"type": "container",
"class": null,
"class": "container",
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 5000.0
"max_volume": 5000.0,
"size_x": 200.0,
"size_y": 200.0,
"size_z": 200.0
},
"data": {
"liquid": [
@@ -71,7 +74,7 @@
"type": "device",
"class": "solenoid_valve.mock",
"position": {
"x": 620.6111111111111,
"x": 780,
"y": 171,
"z": 0
},
@@ -89,7 +92,7 @@
"type": "device",
"class": "vacuum_pump.mock",
"position": {
"x": 620.6111111111111,
"x": 500,
"y": 171,
"z": 0
},
@@ -107,7 +110,7 @@
"type": "device",
"class": "gas_source.mock",
"position": {
"x": 620.6111111111111,
"x": 900,
"y": 171,
"z": 0
},
@@ -119,39 +122,39 @@
],
"links": [
{
"source": "reactor",
"target": "vacuum_valve",
"type": "physical",
"source": "vacuum_valve",
"target": "reactor",
"type": "fluid",
"port": {
"reactor": "top",
"vacuum_valve": "1"
"vacuum_valve": "out"
}
},
{
"source": "reactor",
"target": "gas_valve",
"type": "physical",
"source": "gas_valve",
"target": "reactor",
"type": "fluid",
"port": {
"reactor": "top",
"gas_valve": "1"
"gas_valve": "out"
}
},
{
"source": "vacuum_pump",
"target": "vacuum_valve",
"type": "physical",
"source": "vacuum_valve",
"target": "vacuum_pump",
"type": "fluid",
"port": {
"vacuum_pump": "out",
"vacuum_valve": "0"
"vacuum_valve": "in"
}
},
{
"source": "gas_source",
"target": "gas_valve",
"type": "physical",
"source": "gas_valve",
"target": "gas_source",
"type": "fluid",
"port": {
"gas_source": "out",
"gas_valve": "0"
"gas_valve": "in"
}
}
]

View File

@@ -8,6 +8,7 @@ def start_backend(
backend: str,
devices_config: dict = {},
resources_config: list = [],
resources_edge_config: list = [],
graph=None,
controllers_config: dict = {},
bridges=[],
@@ -31,7 +32,7 @@ def start_backend(
backend_thread = threading.Thread(
target=main if not without_host else slave,
args=(devices_config, resources_config, graph, controllers_config, bridges, visual, resources_mesh_config),
args=(devices_config, resources_config, resources_edge_config, graph, controllers_config, bridges, visual, resources_mesh_config),
name="backend_thread",
daemon=True,
)

View File

@@ -10,7 +10,7 @@ from copy import deepcopy
import yaml
from unilabos.resources.graphio import tree_to_list
from unilabos.resources.graphio import tree_to_list, modify_to_backend_format
# 首先添加项目根目录到路径
current_dir = os.path.dirname(os.path.abspath(__file__))
@@ -22,6 +22,21 @@ from unilabos.config.config import load_config, BasicConfig, _update_config_from
from unilabos.utils.banner_print import print_status, print_unilab_banner
def load_config_from_file(config_path):
if config_path is None:
config_path = os.environ.get("UNILABOS.BASICCONFIG.CONFIG_PATH", None)
if config_path:
if not os.path.exists(config_path):
print_status(f"配置文件 {config_path} 不存在", "error")
elif not config_path.endswith(".py"):
print_status(f"配置文件 {config_path} 不是Python文件必须以.py结尾", "error")
else:
load_config(config_path)
else:
print_status(f"启动 UniLab-OS时配置文件参数未正确传入 --config '{config_path}' 尝试本地配置...", "warning")
load_config(config_path)
def parse_args():
"""解析命令行参数"""
parser = argparse.ArgumentParser(description="Start Uni-Lab Edge server.")
@@ -58,6 +73,11 @@ def parse_args():
action="store_true",
help="Slave模式下跳过等待host服务",
)
parser.add_argument(
"--upload_registry",
action="store_true",
help="启动unilab时同时报送注册表信息",
)
parser.add_argument(
"--config",
type=str,
@@ -97,22 +117,12 @@ def main():
# 加载配置文件优先加载config然后从env读取
config_path = args_dict.get("config")
if config_path is None:
config_path = os.environ.get("UNILABOS.BASICCONFIG.CONFIG_PATH", None)
if config_path:
if not os.path.exists(config_path):
print_status(f"配置文件 {config_path} 不存在", "error")
elif not config_path.endswith(".py"):
print_status(f"配置文件 {config_path} 不是Python文件必须以.py结尾", "error")
else:
load_config(config_path)
else:
print_status(f"启动 UniLab-OS时配置文件参数未正确传入 --config '{config_path}' 尝试本地配置...", "warning")
load_config(config_path)
load_config_from_file(config_path)
# 设置BasicConfig参数
BasicConfig.is_host_mode = not args_dict.get("without_host", False)
BasicConfig.slave_no_host = args_dict.get("slave_no_host", False)
BasicConfig.upload_registry = args_dict.get("upload_registry", False)
machine_name = os.popen("hostname").read().strip()
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
BasicConfig.machine_name = machine_name
@@ -136,15 +146,16 @@ def main():
# 注册表
build_registry(args_dict["registry_path"])
resource_edge_info = []
devices_and_resources = None
if args_dict["graph"] is not None:
import unilabos.resources.graphio as graph_res
graph_res.physical_setup_graph = (
read_node_link_json(args_dict["graph"])
if args_dict["graph"].endswith(".json")
else read_graphml(args_dict["graph"])
)
if args_dict["graph"].endswith(".json"):
graph, data = read_node_link_json(args_dict["graph"])
else:
graph, data = read_graphml(args_dict["graph"])
graph_res.physical_setup_graph = graph
resource_edge_info = modify_to_backend_format(data["links"])
devices_and_resources = dict_from_graph(graph_res.physical_setup_graph)
# args_dict["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values()))
args_dict["resources_config"] = list(devices_and_resources.values())
@@ -185,6 +196,7 @@ def main():
signal.signal(signal.SIGTERM, _exit)
mqtt_client.start()
args_dict["resources_mesh_config"] = {}
args_dict["resources_edge_config"] = resource_edge_info
# web visiualize 2D
if args_dict["visual"] != "disable":
enable_rviz = args_dict["visual"] == "rviz"

View File

@@ -172,13 +172,14 @@ class MQTTClient:
jobdata = {"job_id": job_id, "data": feedback_data, "status": status, "return_info": return_info}
self.client.publish(f"labs/{MQConfig.lab_id}/job/list/", json.dumps(jobdata), qos=2)
def publish_registry(self, device_id: str, device_info: dict):
def publish_registry(self, device_id: str, device_info: dict, print_debug: bool = True):
if self.mqtt_disable:
return
address = f"labs/{MQConfig.lab_id}/registry/"
registry_data = json.dumps({device_id: device_info}, ensure_ascii=False, cls=TypeEncoder)
self.client.publish(address, registry_data, qos=2)
logger.debug(f"Registry data published: address: {address}, {registry_data}")
if print_debug:
logger.debug(f"Registry data published: address: {address}, {registry_data}")
def publish_actions(self, action_id: str, action_info: dict):
if self.mqtt_disable:

67
unilabos/app/register.py Normal file
View File

@@ -0,0 +1,67 @@
import argparse
import time
from unilabos.registry.registry import build_registry
from unilabos.app.main import load_config_from_file
from unilabos.utils.log import logger
def register_devices_and_resources(mqtt_client, lab_registry):
"""
注册设备和资源到 MQTT
"""
logger.info("[UniLab Register] 开始注册设备和资源...")
# 注册设备信息
for device_info in lab_registry.obtain_registry_device_info():
mqtt_client.publish_registry(device_info["id"], device_info, False)
logger.debug(f"[UniLab Register] 注册设备: {device_info['id']}")
# 注册资源信息
for resource_info in lab_registry.obtain_registry_resource_info():
mqtt_client.publish_registry(resource_info["id"], resource_info, False)
logger.debug(f"[UniLab Register] 注册资源: {resource_info['id']}")
time.sleep(10)
logger.info("[UniLab Register] 设备和资源注册完成.")
def main():
"""
命令行入口函数
"""
parser = argparse.ArgumentParser(description="注册设备和资源到 MQTT")
parser.add_argument(
"--registry_path",
type=str,
default=None,
action="append",
help="注册表路径",
)
parser.add_argument(
"--config",
type=str,
default=None,
help="配置文件路径,支持.py格式的Python配置文件",
)
args = parser.parse_args()
# 构建注册表
build_registry(args.registry_path)
load_config_from_file(args.config)
from unilabos.app.mq import mqtt_client
# 连接mqtt
mqtt_client.start()
from unilabos.registry.registry import lab_registry
# 注册设备和资源
register_devices_and_resources(mqtt_client, lab_registry)
if __name__ == "__main__":
main()

View File

@@ -30,7 +30,27 @@ class HTTPClient:
self.auth = MQConfig.lab_id
info(f"HTTPClient 初始化完成: remote_addr={self.remote_addr}")
def resource_add(self, resources: List[Dict[str, Any]], database_process_later:bool) -> requests.Response:
def resource_edge_add(self, resources: List[Dict[str, Any]], database_process_later: bool) -> requests.Response:
"""
添加资源
Args:
resources: 要添加的资源列表
database_process_later: 后台处理资源
Returns:
Response: API响应对象
"""
response = requests.post(
f"{self.remote_addr}/lab/resource/edge/batch_create/?database_process_later={1 if database_process_later else 0}",
json=resources,
headers={"Authorization": f"lab {self.auth}"},
timeout=100,
)
if response.status_code != 200 and response.status_code != 201:
logger.error(f"添加物料关系失败: {response.status_code}, {response.text}")
return response
def resource_add(self, resources: List[Dict[str, Any]], database_process_later: bool) -> requests.Response:
"""
添加资源
@@ -44,8 +64,10 @@ class HTTPClient:
f"{self.remote_addr}/lab/resource/?database_process_later={1 if database_process_later else 0}",
json=resources,
headers={"Authorization": f"lab {self.auth}"},
timeout=5,
timeout=100,
)
if response.status_code != 200:
logger.error(f"添加物料失败: {response.text}")
return response
def resource_get(self, id: str, with_children: bool = False) -> Dict[str, Any]:
@@ -63,7 +85,7 @@ class HTTPClient:
f"{self.remote_addr}/lab/resource/?edge_format=1",
params={"id": id, "with_children": with_children},
headers={"Authorization": f"lab {self.auth}"},
timeout=5,
timeout=20,
)
return response.json()
@@ -81,7 +103,7 @@ class HTTPClient:
f"{self.remote_addr}/lab/resource/batch_delete/",
params={"id": id},
headers={"Authorization": f"lab {self.auth}"},
timeout=5,
timeout=20,
)
return response
@@ -99,7 +121,7 @@ class HTTPClient:
f"{self.remote_addr}/lab/resource/batch_update/?edge_format=1",
json=resources,
headers={"Authorization": f"lab {self.auth}"},
timeout=5,
timeout=100,
)
return response

View File

@@ -16,7 +16,6 @@ from jinja2 import Environment, FileSystemLoader
from unilabos.config.config import BasicConfig
from unilabos.registry.registry import lab_registry
from unilabos.app.mq import mqtt_client
from unilabos.ros.msgs.message_converter import msg_converter_manager
from unilabos.utils.log import error
from unilabos.utils.type_check import TypeEncoder

View File

@@ -15,46 +15,116 @@ def generate_add_protocol(
purpose: str
) -> List[Dict[str, Any]]:
"""
生成添加试剂的协议序列 - 严格按照 Add.action
生成添加试剂的协议序列
流程:
1. 找到包含目标试剂的试剂瓶
2. 配置八通阀门到试剂瓶位置
3. 使用注射器泵吸取试剂
4. 配置八通阀门到反应器位置
5. 使用注射器泵推送试剂到反应器
6. 如果需要,启动搅拌
"""
action_sequence = []
# 验证目标容器存在
if vessel not in G.nodes():
raise ValueError(f"目标容器 {vessel} 不存在")
# 如果指定了体积,执行液体转移
if volume > 0:
# 查找可用的试剂瓶
# 1. 查找注射器泵 (transfer pump)
transfer_pump_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_transfer_pump']
if not transfer_pump_nodes:
raise ValueError("没有找到可用的注射器泵 (virtual_transfer_pump)")
transfer_pump_id = transfer_pump_nodes[0]
# 2. 查找八通阀门
multiway_valve_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_multiway_valve']
if not multiway_valve_nodes:
raise ValueError("没有找到可用的八通阀门 (virtual_multiway_valve)")
valve_id = multiway_valve_nodes[0]
# 3. 查找包含指定试剂的试剂瓶
reagent_vessel = None
available_flasks = [node for node in G.nodes()
if node.startswith('flask_')
and G.nodes[node].get('type') == 'container']
if not available_flasks:
# 简化:使用第一个可用的试剂瓶,实际应该根据试剂名称匹配
if available_flasks:
reagent_vessel = available_flasks[0]
else:
raise ValueError("没有找到可用的试剂容器")
reagent_vessel = available_flasks[0]
# 查找泵设备
pump_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_pump']
# 4. 获取试剂瓶和反应器对应的阀门位置
# 这需要根据实际连接图来确定,这里假设:
reagent_valve_position = 1 # 试剂瓶连接到阀门位置1
reactor_valve_position = 2 # 反应器连接到阀门位置2
if pump_nodes:
pump_id = pump_nodes[0]
action_sequence.append({
"device_id": pump_id,
"action_name": "transfer",
"action_kwargs": {
"from_vessel": reagent_vessel,
"to_vessel": vessel,
"volume": volume,
"amount": amount,
"time": time,
"viscous": viscous,
"rinsing_solvent": "",
"rinsing_volume": 0.0,
"rinsing_repeats": 0,
"solid": False
}
})
# 5. 执行添加操作序列
# 5.1 设置阀门到试剂瓶位置
action_sequence.append({
"device_id": valve_id,
"action_name": "set_position",
"action_kwargs": {
"position": reagent_valve_position
}
})
# 5.2 使用注射器泵从试剂瓶吸取液体
action_sequence.append({
"device_id": transfer_pump_id,
"action_name": "transfer",
"action_kwargs": {
"from_vessel": reagent_vessel,
"to_vessel": transfer_pump_id, # 吸入到注射器
"volume": volume,
"amount": amount,
"time": time / 2, # 吸取时间为总时间的一半
"viscous": viscous,
"rinsing_solvent": "",
"rinsing_volume": 0.0,
"rinsing_repeats": 0,
"solid": False
}
})
# 5.3 设置阀门到反应器位置
action_sequence.append({
"device_id": valve_id,
"action_name": "set_position",
"action_kwargs": {
"position": reactor_valve_position
}
})
# 5.4 使用注射器泵将液体推送到反应器
action_sequence.append({
"device_id": transfer_pump_id,
"action_name": "transfer",
"action_kwargs": {
"from_vessel": transfer_pump_id, # 从注射器推出
"to_vessel": vessel,
"volume": volume,
"amount": amount,
"time": time / 2, # 推送时间为总时间的一半
"viscous": viscous,
"rinsing_solvent": "",
"rinsing_volume": 0.0,
"rinsing_repeats": 0,
"solid": False
}
})
# 如果需要搅拌,使用 StartStir 而不是 Stir
# 6. 如果需要搅拌,启动搅拌器
if stir:
stirrer_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_stirrer']
@@ -63,12 +133,156 @@ def generate_add_protocol(
stirrer_id = stirrer_nodes[0]
action_sequence.append({
"device_id": stirrer_id,
"action_name": "start_stir", # 使用 start_stir 而不是 stir
"action_name": "start_stir",
"action_kwargs": {
"vessel": vessel,
"stir_speed": stir_speed,
"purpose": f"添加 {reagent} 后搅拌"
"purpose": f"添加 {reagent} 后搅拌混合"
}
})
else:
print("警告:需要搅拌但未找到搅拌设备")
return action_sequence
def find_valve_position_for_vessel(G: nx.DiGraph, valve_id: str, vessel_id: str) -> int:
"""
根据连接图找到容器对应的阀门位置
Args:
G: 网络图
valve_id: 阀门设备ID
vessel_id: 容器ID
Returns:
int: 阀门位置编号 (1-8)
"""
# 查找阀门到容器的连接
edges = G.edges(data=True)
for source, target, data in edges:
if source == valve_id and target == vessel_id:
# 从连接数据中提取端口信息
port_info = data.get('port', {})
valve_port = port_info.get(valve_id, '')
# 解析端口名称获取位置编号
if valve_port.startswith('multiway-valve-port-'):
position = valve_port.split('-')[-1]
return int(position)
# 默认返回位置1
return 1
def generate_add_with_autodiscovery(
G: nx.DiGraph,
vessel: str,
reagent: str,
volume: float,
**kwargs
) -> List[Dict[str, Any]]:
"""
智能添加协议生成器 - 自动发现设备连接关系
"""
action_sequence = []
# 查找必需的设备
devices = {
'transfer_pump': None,
'multiway_valve': None,
'stirrer': None
}
for node in G.nodes():
node_class = G.nodes[node].get('class')
if node_class == 'virtual_transfer_pump':
devices['transfer_pump'] = node
elif node_class == 'virtual_multiway_valve':
devices['multiway_valve'] = node
elif node_class == 'virtual_stirrer':
devices['stirrer'] = node
# 验证必需设备
if not devices['transfer_pump']:
raise ValueError("缺少注射器泵设备")
if not devices['multiway_valve']:
raise ValueError("缺少八通阀门设备")
# 查找试剂容器
reagent_vessels = [node for node in G.nodes()
if node.startswith('flask_')
and G.nodes[node].get('type') == 'container']
if not reagent_vessels:
raise ValueError("没有找到试剂容器")
# 执行添加流程
reagent_vessel = reagent_vessels[0]
reagent_pos = find_valve_position_for_vessel(G, devices['multiway_valve'], reagent_vessel)
reactor_pos = find_valve_position_for_vessel(G, devices['multiway_valve'], vessel)
# 生成操作序列
action_sequence.extend([
# 切换到试剂瓶
{
"device_id": devices['multiway_valve'],
"action_name": "set_position",
"action_kwargs": {"position": reagent_pos}
},
# 吸取试剂
{
"device_id": devices['transfer_pump'],
"action_name": "transfer",
"action_kwargs": {
"from_vessel": reagent_vessel,
"to_vessel": devices['transfer_pump'],
"volume": volume,
"amount": kwargs.get('amount', ''),
"time": kwargs.get('time', 10.0) / 2,
"viscous": kwargs.get('viscous', False),
"rinsing_solvent": "",
"rinsing_volume": 0.0,
"rinsing_repeats": 0,
"solid": False
}
},
# 切换到反应器
{
"device_id": devices['multiway_valve'],
"action_name": "set_position",
"action_kwargs": {"position": reactor_pos}
},
# 推送到反应器
{
"device_id": devices['transfer_pump'],
"action_name": "transfer",
"action_kwargs": {
"from_vessel": devices['transfer_pump'],
"to_vessel": vessel,
"volume": volume,
"amount": kwargs.get('amount', ''),
"time": kwargs.get('time', 10.0) / 2,
"viscous": kwargs.get('viscous', False),
"rinsing_solvent": "",
"rinsing_volume": 0.0,
"rinsing_repeats": 0,
"solid": False
}
}
])
# 如果需要搅拌
if kwargs.get('stir', False) and devices['stirrer']:
action_sequence.append({
"device_id": devices['stirrer'],
"action_name": "start_stir",
"action_kwargs": {
"vessel": vessel,
"stir_speed": kwargs.get('stir_speed', 300.0),
"purpose": f"添加 {reagent} 后混合"
}
})
return action_sequence

View File

@@ -2,17 +2,42 @@ import numpy as np
import networkx as nx
def is_integrated_pump(node_name):
return "pump" in node_name and "valve" in node_name
def find_connected_pump(G, valve_node):
for neighbor in G.neighbors(valve_node):
if "pump" in G.nodes[neighbor]["class"]:
return neighbor
raise ValueError(f"未找到与阀 {valve_node} 唯一相连的泵节点")
def build_pump_valve_maps(G, pump_backbone):
pumps_from_node = {}
valve_from_node = {}
for node in pump_backbone:
if is_integrated_pump(node):
pumps_from_node[node] = node
valve_from_node[node] = node
else:
pump_node = find_connected_pump(G, node)
pumps_from_node[node] = pump_node
valve_from_node[node] = node
return pumps_from_node, valve_from_node
def generate_pump_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
volume: float,
flowrate: float = 0.5,
transfer_flowrate: float = 0,
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
volume: float,
flowrate: float = 0.5,
transfer_flowrate: float = 0,
) -> list[dict]:
"""
生成泵操作的动作序列。
:param G: 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置
:param from_vessel: 容器A
:param to_vessel: 容器B
@@ -21,194 +46,137 @@ def generate_pump_protocol(
:param transfer_flowrate: 泵骨架中转移流速(若不指定,默认与注入流速相同)
:return: 泵操作的动作序列
"""
# 生成泵操作的动作序列
pump_action_sequence = []
# 检查节点是否存在
if from_vessel not in G.nodes:
print(f"Warning: Source vessel '{from_vessel}' not found in graph. Skipping.")
return []
if to_vessel not in G.nodes:
print(f"Warning: Target vessel '{to_vessel}' not found in graph. Skipping.")
return []
# 检查是否存在路径
try:
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
except nx.NetworkXNoPath:
print(f"Warning: No path from '{from_vessel}' to '{to_vessel}'. Skipping.")
return []
except nx.NodeNotFound as e:
print(f"Warning: Node not found: {e}. Skipping.")
return []
print(f"Shortest path: {shortest_path}")
nodes = G.nodes(data=True)
# 从from_vessel到to_vessel的最短路径
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
print(shortest_path)
pump_backbone = shortest_path
if not from_vessel.startswith("pump"):
pump_backbone = pump_backbone[1:]
if not to_vessel.startswith("pump"):
pump_backbone = pump_backbone[:-1]
print(f"Pump backbone: {pump_backbone}")
# 修复检查pump_backbone是否为空
if not pump_backbone:
print(f"Warning: No pumps found in path from '{from_vessel}' to '{to_vessel}'. Skipping.")
return []
if transfer_flowrate == 0:
transfer_flowrate = flowrate
# 修复:正确访问节点数据
pump_max_volumes = []
for pump in pump_backbone:
# 直接使用 G.nodes[pump] 来访问节点数据
pump_data = G.nodes[pump] if pump in G.nodes else {}
# 尝试多种可能的键名,并提供默认值
max_vol = pump_data.get('max_volume') or pump_data.get('max_vol') or pump_data.get('volume')
if max_vol is None:
# 如果是设备节点尝试从config中获取
config = pump_data.get('config', {})
max_vol = config.get('max_volume', 25.0)
pump_max_volumes.append(float(max_vol))
if pump_max_volumes:
min_transfer_volume = min(pump_max_volumes)
else:
min_transfer_volume = 25.0 # 默认值
pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone)
min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone])
repeats = int(np.ceil(volume / min_transfer_volume))
if repeats > 1 and (from_vessel.startswith("pump") or to_vessel.startswith("pump")):
raise ValueError("Cannot transfer volume larger than min_transfer_volume between two pumps.")
volume_left = volume
# 生成泵操作的动作序列
for i in range(repeats):
# 单泵依次执行阀指令、活塞指令,将液体吸入与之相连的第一台泵
if not from_vessel.startswith("pump") and pump_backbone:
# 修复:添加边缘数据检查
edge_data = G.get_edge_data(pump_backbone[0], from_vessel)
if edge_data and "port" in edge_data:
pump_action_sequence.extend([
{
"device_id": pump_backbone[0],
"action_name": "set_valve_position",
"action_kwargs": {
"command": edge_data["port"][pump_backbone[0]]
}
},
{
"device_id": pump_backbone[0],
"action_name": "set_position",
"action_kwargs": {
"position": float(min(volume_left, min_transfer_volume)),
"max_velocity": transfer_flowrate
}
if not from_vessel.startswith("pump"):
pump_action_sequence.extend([
{
"device_id": valve_from_node[pump_backbone[0]],
"action_name": "set_valve_position",
"action_kwargs": {
"command": G.get_edge_data(pump_backbone[0], from_vessel)["port"][pump_backbone[0]]
}
])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
else:
print(f"Warning: No edge data found between {pump_backbone[0]} and {from_vessel}")
# 修复检查pump_backbone长度避免多泵操作时出错
if len(pump_backbone) > 1:
for pumpA, pumpB in zip(pump_backbone[:-1], pump_backbone[1:]):
# 相邻两泵同时切换阀门至连通位置
edge_AB = G.get_edge_data(pumpA, pumpB)
edge_BA = G.get_edge_data(pumpB, pumpA)
if edge_AB and "port" in edge_AB and edge_BA and "port" in edge_BA:
pump_action_sequence.append([
{
"device_id": pumpA,
"action_name": "set_valve_position",
"action_kwargs": {
"command": edge_AB["port"][pumpA]
}
},
{
"device_id": pumpB,
"action_name": "set_valve_position",
"action_kwargs": {
"command": edge_BA["port"][pumpB],
}
},
{
"device_id": pumps_from_node[pump_backbone[0]],
"action_name": "set_position",
"action_kwargs": {
"position": float(min(volume_left, min_transfer_volume)),
"max_velocity": transfer_flowrate
}
])
# 相邻两泵液体转移泵A排出液体泵B吸入液体
pump_action_sequence.append([
{
"device_id": pumpA,
"action_name": "set_position",
"action_kwargs": {
"position": 0.0,
"max_velocity": transfer_flowrate
}
},
{
"device_id": pumpB,
"action_name": "set_position",
"action_kwargs": {
"position": float(min(volume_left, min_transfer_volume)),
"max_velocity": transfer_flowrate
}
}
])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
for nodeA, nodeB in zip(pump_backbone[:-1], pump_backbone[1:]):
# 相邻两泵同时切换阀门至连通位置
pump_action_sequence.append([
{
"device_id": valve_from_node[nodeA],
"action_name": "set_valve_position",
"action_kwargs": {
"command": G.get_edge_data(nodeA, nodeB)["port"][nodeA]
}
])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
else:
print(f"Warning: No edge data found between {pumpA} and {pumpB}")
if not to_vessel.startswith("pump") and pump_backbone:
},
{
"device_id": valve_from_node[nodeB],
"action_name": "set_valve_position",
"action_kwargs": {
"command": G.get_edge_data(nodeB, nodeA)["port"][nodeB],
}
}
])
# 相邻两泵液体转移泵A排出液体泵B吸入液体
pump_action_sequence.append([
{
"device_id": pumps_from_node[nodeA],
"action_name": "set_position",
"action_kwargs": {
"position": 0.0,
"max_velocity": transfer_flowrate
}
},
{
"device_id": pumps_from_node[nodeB],
"action_name": "set_position",
"action_kwargs": {
"position": float(min(volume_left, min_transfer_volume)),
"max_velocity": transfer_flowrate
}
}
])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
if not to_vessel.startswith("pump"):
# 单泵依次执行阀指令、活塞指令将最后一台泵液体缓慢加入容器B
edge_data = G.get_edge_data(pump_backbone[-1], to_vessel)
if edge_data and "port" in edge_data:
pump_action_sequence.extend([
{
"device_id": pump_backbone[-1],
"action_name": "set_valve_position",
"action_kwargs": {
"command": edge_data["port"][pump_backbone[-1]]
}
},
{
"device_id": pump_backbone[-1],
"action_name": "set_position",
"action_kwargs": {
"position": 0.0,
"max_velocity": flowrate
}
pump_action_sequence.extend([
{
"device_id": valve_from_node[pump_backbone[-1]],
"action_name": "set_valve_position",
"action_kwargs": {
"command": G.get_edge_data(pump_backbone[-1], to_vessel)["port"][pump_backbone[-1]]
}
])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
else:
print(f"Warning: No edge data found between {pump_backbone[-1]} and {to_vessel}")
},
{
"device_id": pumps_from_node[pump_backbone[-1]],
"action_name": "set_position",
"action_kwargs": {
"position": 0.0,
"max_velocity": flowrate
}
}
])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
volume_left -= min_transfer_volume
return pump_action_sequence
# Pump protocol compilation
def generate_pump_protocol_with_rinsing(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
volume: float,
amount: str = "",
time: float = 0,
viscous: bool = False,
rinsing_solvent: str = "air",
rinsing_volume: float = 5.0,
rinsing_repeats: int = 2,
solid: bool = False,
flowrate: float = 2.5,
transfer_flowrate: float = 0.5,
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
volume: float,
amount: str = "",
time: float = 0,
viscous: bool = False,
rinsing_solvent: str = "air",
rinsing_volume: float = 5.0,
rinsing_repeats: int = 2,
solid: bool = False,
flowrate: float = 2.5,
transfer_flowrate: float = 0.5,
) -> list[dict]:
"""
Generates a pump protocol for transferring a specified volume between vessels, including rinsing steps with a chosen solvent. This function constructs a sequence of pump actions based on the provided parameters and the shortest path in a directed graph.
Args:
G (nx.DiGraph): The directed graph representing the vessels and connections. 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置
from_vessel (str): The name of the vessel to transfer from.
@@ -223,96 +191,64 @@ def generate_pump_protocol_with_rinsing(
solid (bool, optional): Indicates if the transfer involves a solid (default is False).
flowrate (float, optional): The flow rate for the transfer (default is 2.5). 最终注入容器B时的流速
transfer_flowrate (float, optional): The flow rate for the transfer action (default is 0.5). 泵骨架中转移流速(若不指定,默认与注入流速相同)
Returns:
list[dict]: A sequence of pump actions to be executed for the transfer and rinsing process. 泵操作的动作序列.
Raises:
AssertionError: If the number of rinsing solvents does not match the number of rinsing repeats.
Examples:
pump_protocol = generate_pump_protocol_with_rinsing(G, "vessel_A", "vessel_B", 0.1, rinsing_solvent="water")
"""
# 修复:使用实际存在的节点名称
air_vessel = "flask_air" # 这个在你的配置中存在
# 寻找合适的废料容器,如果没有找到则使用空的容器作为替代
waste_vessel = None
available_vessels = [node for node in G.nodes if node.startswith("flask_") and node != air_vessel]
if available_vessels:
# 使用第一个可用的容器作为废料容器
waste_vessel = available_vessels[0]
print(f"Using {waste_vessel} as waste vessel")
else:
waste_vessel = "flask_1" # 备用选择
# 修复:添加路径检查
try:
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
pump_backbone = shortest_path[1: -1]
except (nx.NetworkXNoPath, nx.NodeNotFound) as e:
print(f"Warning: Cannot find path from {from_vessel} to {to_vessel}: {e}")
return []
# 修复:正确访问节点数据
pump_max_volumes = []
for pump in pump_backbone:
# 直接使用 G.nodes[pump] 来访问节点数据
pump_data = G.nodes[pump] if pump in G.nodes else {}
# 尝试多种可能的键名,并提供默认值
max_vol = pump_data.get('max_volume') or pump_data.get('max_vol') or pump_data.get('volume')
if max_vol is None:
# 如果是设备节点尝试从config中获取
config = pump_data.get('config', {})
max_vol = config.get('max_volume', 25.0)
pump_max_volumes.append(float(max_vol))
if pump_max_volumes:
min_transfer_volume = float(min(pump_max_volumes))
else:
min_transfer_volume = 25.0 # 默认值
air_vessel = "flask_air"
waste_vessel = f"waste_workup"
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
pump_backbone = shortest_path[1: -1]
nodes = G.nodes(data=True)
pumps_from_node, valve_from_node = build_pump_valve_maps(G, pump_backbone)
min_transfer_volume = min([nodes[pumps_from_node[node]]["config"]["max_volume"] for node in pump_backbone])
if time != 0:
flowrate = transfer_flowrate = volume / time
pump_action_sequence = generate_pump_protocol(G, from_vessel, to_vessel, float(volume), flowrate, transfer_flowrate)
# 修复:只在需要清洗且相关节点存在时才执行清洗步骤
if rinsing_solvent != "air" and pump_backbone:
if rinsing_solvent != "air" and rinsing_solvent != "":
if "," in rinsing_solvent:
rinsing_solvents = rinsing_solvent.split(",")
assert len(rinsing_solvents) == rinsing_repeats, "Number of rinsing solvents must match number of rinsing repeats."
assert len(
rinsing_solvents) == rinsing_repeats, "Number of rinsing solvents must match number of rinsing repeats."
else:
rinsing_solvents = [rinsing_solvent] * rinsing_repeats
for rinsing_solvent in rinsing_solvents:
solvent_vessel = f"flask_{rinsing_solvent}"
# 检查溶剂容器是否存在
if solvent_vessel not in G.nodes:
print(f"Warning: Solvent vessel '{solvent_vessel}' not found in graph. Skipping rinsing step.")
continue
# 清洗泵 - 只有当所有必需的节点都存在且pump_backbone不为空时才执行
if pump_backbone and len(pump_backbone) > 0 and waste_vessel in G.nodes:
# 清洗泵
pump_action_sequence.extend(
generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate,
transfer_flowrate) +
generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate,
transfer_flowrate) +
generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate,
transfer_flowrate)
)
# 如果转移的是溶液,第一种冲洗溶剂请选用溶液的溶剂,稀释泵内、转移管道内的溶液。后续冲洗溶剂不需要此操作。
if rinsing_solvent == rinsing_solvents[0]:
pump_action_sequence.extend(
generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate, transfer_flowrate) +
generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate, transfer_flowrate) +
generate_pump_protocol(G, pump_backbone[-1], waste_vessel, min_transfer_volume, flowrate, transfer_flowrate)
)
# 如果转移的是溶液,第一种冲洗溶剂请选用溶液的溶剂,稀释泵内、转移管道内的溶液。后续冲洗溶剂不需要此操作。
if rinsing_solvent == rinsing_solvents[0]:
pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate))
pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate))
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, solvent_vessel, rinsing_volume, flowrate, transfer_flowrate))
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate))
# 最后的空气清洗 - 只有当节点存在时才执行
if air_vessel in G.nodes:
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
generate_pump_protocol(G, solvent_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate))
pump_action_sequence.extend(
generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate))
pump_action_sequence.extend(
generate_pump_protocol(G, air_vessel, solvent_vessel, rinsing_volume, flowrate, transfer_flowrate))
pump_action_sequence.extend(
generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate))
if rinsing_solvent != "":
pump_action_sequence.extend(
generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
pump_action_sequence.extend(
generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2)
return pump_action_sequence
# End Protocols

View File

@@ -10,8 +10,9 @@ from unilabos.utils import logger
class BasicConfig:
ENV = "pro" # 'test'
config_path = ""
is_host_mode = True # 从registry.py移动过来
is_host_mode = True
slave_no_host = False # 是否跳过rclient.wait_for_service()
upload_registry = False
machine_name = "undefined"
vis_2d_enable = False

View File

@@ -0,0 +1,46 @@
import time
from typing import Dict, Any, Optional
class VirtualGasSource:
"""Virtual gas source for testing"""
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
self.device_id = device_id or "unknown_gas_source"
self.config = config or {}
self.data = {}
self._status = "OPEN"
async def initialize(self) -> bool:
"""Initialize virtual gas source"""
self.data.update({
"status": self._status
})
return True
async def cleanup(self) -> bool:
"""Cleanup virtual gas source"""
return True
@property
def status(self) -> str:
return self._status
def get_status(self) -> str:
return self._status
def set_status(self, string):
self._status = string
time.sleep(5)
def open(self):
self._status = "OPEN"
def close(self):
self._status = "CLOSED"
def is_open(self):
return self._status
def is_closed(self):
return not self._status

View File

@@ -0,0 +1,221 @@
import time
from typing import Union, Dict, Optional
class VirtualMultiwayValve:
"""
虚拟九通阀门 - 0号位连接transfer pump1-8号位连接其他设备
"""
def __init__(self, port: str = "VIRTUAL", positions: int = 8):
self.port = port
self.max_positions = positions # 1-8号位
self.total_positions = positions + 1 # 0-8号位共9个位置
# 状态属性
self._status = "Idle"
self._valve_state = "Ready"
self._current_position = 0 # 默认在0号位transfer pump位置
self._target_position = 0
# 位置映射说明
self.position_map = {
0: "transfer_pump", # 0号位连接转移泵
1: "port_1", # 1号位
2: "port_2", # 2号位
3: "port_3", # 3号位
4: "port_4", # 4号位
5: "port_5", # 5号位
6: "port_6", # 6号位
7: "port_7", # 7号位
8: "port_8" # 8号位
}
@property
def status(self) -> str:
return self._status
@property
def valve_state(self) -> str:
return self._valve_state
@property
def current_position(self) -> int:
return self._current_position
@property
def target_position(self) -> int:
return self._target_position
def get_current_position(self) -> int:
"""获取当前阀门位置"""
return self._current_position
def get_current_port(self) -> str:
"""获取当前连接的端口名称"""
return self.position_map.get(self._current_position, "unknown")
def set_position(self, command: Union[int, str]):
"""
设置阀门位置 - 支持0-8位置
Args:
command: 目标位置 (0-8) 或位置字符串
0: transfer pump位置
1-8: 其他设备位置
"""
try:
# 如果是字符串形式的位置,先转换为数字
if isinstance(command, str):
pos = int(command)
else:
pos = int(command)
if pos < 0 or pos > self.max_positions:
raise ValueError(f"Position must be between 0 and {self.max_positions}")
self._status = "Busy"
self._valve_state = "Moving"
self._target_position = pos
# 模拟阀门切换时间
switch_time = abs(self._current_position - pos) * 0.5 # 每个位置0.5秒
time.sleep(switch_time)
self._current_position = pos
self._status = "Idle"
self._valve_state = "Ready"
current_port = self.get_current_port()
return f"Position set to {pos} ({current_port})"
except ValueError as e:
self._status = "Error"
self._valve_state = "Error"
return f"Error: {str(e)}"
def set_to_pump_position(self):
"""切换到transfer pump位置0号位"""
return self.set_position(0)
def set_to_port(self, port_number: int):
"""
切换到指定端口位置
Args:
port_number: 端口号 (1-8)
"""
if port_number < 1 or port_number > self.max_positions:
raise ValueError(f"Port number must be between 1 and {self.max_positions}")
return self.set_position(port_number)
def open(self):
"""打开阀门 - 设置到transfer pump位置0号位"""
return self.set_to_pump_position()
def close(self):
"""关闭阀门 - 对于多通阀门,设置到一个"关闭"状态"""
self._status = "Busy"
self._valve_state = "Closing"
time.sleep(0.5)
# 可以选择保持当前位置或设置特殊关闭状态
self._status = "Idle"
self._valve_state = "Closed"
return f"Valve closed at position {self._current_position}"
def get_valve_position(self) -> int:
"""获取阀门位置 - 兼容性方法"""
return self._current_position
def is_at_position(self, position: int) -> bool:
"""检查是否在指定位置"""
return self._current_position == position
def is_at_pump_position(self) -> bool:
"""检查是否在transfer pump位置"""
return self._current_position == 0
def is_at_port(self, port_number: int) -> bool:
"""检查是否在指定端口位置"""
return self._current_position == port_number
def get_available_positions(self) -> list:
"""获取可用位置列表"""
return list(range(0, self.max_positions + 1))
def get_available_ports(self) -> Dict[int, str]:
"""获取可用端口映射"""
return self.position_map.copy()
def reset(self):
"""重置阀门到transfer pump位置0号位"""
return self.set_position(0)
def switch_between_pump_and_port(self, port_number: int):
"""
在transfer pump位置和指定端口之间切换
Args:
port_number: 目标端口号 (1-8)
"""
if self._current_position == 0:
# 当前在pump位置切换到指定端口
return self.set_to_port(port_number)
else:
# 当前在某个端口切换到pump位置
return self.set_to_pump_position()
def get_flow_path(self) -> str:
"""获取当前流路路径描述"""
current_port = self.get_current_port()
if self._current_position == 0:
return f"Transfer pump connected (position {self._current_position})"
else:
return f"Port {self._current_position} connected ({current_port})"
def get_info(self) -> dict:
"""获取阀门详细信息"""
return {
"port": self.port,
"max_positions": self.max_positions,
"total_positions": self.total_positions,
"current_position": self._current_position,
"current_port": self.get_current_port(),
"target_position": self._target_position,
"status": self._status,
"valve_state": self._valve_state,
"flow_path": self.get_flow_path(),
"position_map": self.position_map
}
def __str__(self):
return f"VirtualMultiwayValve(Position: {self._current_position}/{self.max_positions}, Port: {self.get_current_port()}, Status: {self._status})"
# 使用示例
if __name__ == "__main__":
valve = VirtualMultiwayValve()
print("=== 虚拟九通阀门测试 ===")
print(f"初始状态: {valve}")
print(f"当前流路: {valve.get_flow_path()}")
# 切换到试剂瓶11号位
print(f"\n切换到1号位: {valve.set_position(1)}")
print(f"当前状态: {valve}")
# 切换到transfer pump位置0号位
print(f"\n切换到pump位置: {valve.set_to_pump_position()}")
print(f"当前状态: {valve}")
# 切换到试剂瓶22号位
print(f"\n切换到2号位: {valve.set_to_port(2)}")
print(f"当前状态: {valve}")
# 显示所有可用位置
print(f"\n可用位置: {valve.get_available_positions()}")
print(f"端口映射: {valve.get_available_ports()}")
# 获取详细信息
print(f"\n详细信息: {valve.get_info()}")

View File

@@ -0,0 +1,172 @@
import asyncio
import logging
from typing import Dict, Any, Optional
class VirtualRotavap:
"""Virtual rotary evaporator device for EvaporateProtocol testing"""
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
# 处理可能的不同调用方式
if device_id is None and "id" in kwargs:
device_id = kwargs.pop("id")
if config is None and "config" in kwargs:
config = kwargs.pop("config")
# 设置默认值
self.device_id = device_id or "unknown_rotavap"
self.config = config or {}
self.logger = logging.getLogger(f"VirtualRotavap.{self.device_id}")
self.data = {}
# 添加调试信息
print(f"=== VirtualRotavap {self.device_id} is being created! ===")
print(f"=== Config: {self.config} ===")
print(f"=== Kwargs: {kwargs} ===")
# 从config或kwargs中获取配置参数
self.port = self.config.get("port") or kwargs.get("port", "VIRTUAL")
self._max_temp = self.config.get("max_temp") or kwargs.get("max_temp", 180.0)
self._max_rotation_speed = self.config.get("max_rotation_speed") or kwargs.get("max_rotation_speed", 280.0)
# 处理其他kwargs参数但跳过已知的配置参数
skip_keys = {"port", "max_temp", "max_rotation_speed"}
for key, value in kwargs.items():
if key not in skip_keys and not hasattr(self, key):
setattr(self, key, value)
async def initialize(self) -> bool:
"""Initialize virtual rotary evaporator"""
print(f"=== VirtualRotavap {self.device_id} initialize() called! ===")
self.logger.info(f"Initializing virtual rotary evaporator {self.device_id}")
self.data.update(
{
"status": "Idle",
"rotavap_state": "Ready",
"current_temp": 25.0,
"target_temp": 25.0,
"max_temp": self._max_temp,
"rotation_speed": 0.0,
"max_rotation_speed": self._max_rotation_speed,
"vacuum_pressure": 1.0, # atmospheric pressure
"evaporated_volume": 0.0,
"progress": 0.0,
"message": "",
}
)
return True
async def cleanup(self) -> bool:
"""Cleanup virtual rotary evaporator"""
self.logger.info(f"Cleaning up virtual rotary evaporator {self.device_id}")
return True
async def evaporate(
self, vessel: str, pressure: float = 0.5, temp: float = 60.0, time: float = 300.0, stir_speed: float = 100.0
) -> bool:
"""Execute evaporate action - matches Evaporate action"""
self.logger.info(f"Evaporate: vessel={vessel}, pressure={pressure}, temp={temp}, time={time}")
# 验证参数
if temp > self._max_temp:
self.logger.error(f"Temperature {temp} exceeds maximum {self._max_temp}")
self.data["message"] = f"温度 {temp} 超过最大值 {self._max_temp}"
return False
if stir_speed > self._max_rotation_speed:
self.logger.error(f"Rotation speed {stir_speed} exceeds maximum {self._max_rotation_speed}")
self.data["message"] = f"旋转速度 {stir_speed} 超过最大值 {self._max_rotation_speed}"
return False
if pressure < 0.01 or pressure > 1.0:
self.logger.error(f"Pressure {pressure} bar is out of valid range (0.01-1.0)")
self.data["message"] = f"真空度 {pressure} bar 超出有效范围 (0.01-1.0)"
return False
# 开始蒸发
self.data.update(
{
"status": "Running",
"rotavap_state": "Evaporating",
"target_temp": temp,
"current_temp": temp,
"rotation_speed": stir_speed,
"vacuum_pressure": pressure,
"vessel": vessel,
"target_time": time,
"progress": 0.0,
"message": f"正在蒸发: {vessel}",
}
)
# 模拟蒸发过程
simulation_time = min(time / 60.0, 10.0) # 最多模拟10秒
for progress in range(0, 101, 10):
await asyncio.sleep(simulation_time / 10)
self.data["progress"] = progress
self.data["evaporated_volume"] = progress * 0.5 # 假设最多蒸发50mL
# 蒸发完成
evaporated_vol = 50.0 # 假设蒸发了50mL
self.data.update(
{
"status": "Idle",
"rotavap_state": "Ready",
"current_temp": 25.0,
"target_temp": 25.0,
"rotation_speed": 0.0,
"vacuum_pressure": 1.0,
"evaporated_volume": evaporated_vol,
"progress": 100.0,
"message": f"蒸发完成: {evaporated_vol}mL",
}
)
self.logger.info(f"Evaporation completed: {evaporated_vol}mL from {vessel}")
return True
# 状态属性
@property
def status(self) -> str:
return self.data.get("status", "Unknown")
@property
def rotavap_state(self) -> str:
return self.data.get("rotavap_state", "Unknown")
@property
def current_temp(self) -> float:
return self.data.get("current_temp", 25.0)
@property
def target_temp(self) -> float:
return self.data.get("target_temp", 25.0)
@property
def max_temp(self) -> float:
return self.data.get("max_temp", self._max_temp)
@property
def rotation_speed(self) -> float:
return self.data.get("rotation_speed", 0.0)
@property
def max_rotation_speed(self) -> float:
return self.data.get("max_rotation_speed", self._max_rotation_speed)
@property
def vacuum_pressure(self) -> float:
return self.data.get("vacuum_pressure", 1.0)
@property
def evaporated_volume(self) -> float:
return self.data.get("evaporated_volume", 0.0)
@property
def progress(self) -> float:
return self.data.get("progress", 0.0)
@property
def message(self) -> str:
return self.data.get("message", "")

View File

@@ -0,0 +1,184 @@
import asyncio
import logging
from typing import Dict, Any, Optional
class VirtualSeparator:
"""Virtual separator device for SeparateProtocol testing"""
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
# 处理可能的不同调用方式
if device_id is None and "id" in kwargs:
device_id = kwargs.pop("id")
if config is None and "config" in kwargs:
config = kwargs.pop("config")
# 设置默认值
self.device_id = device_id or "unknown_separator"
self.config = config or {}
self.logger = logging.getLogger(f"VirtualSeparator.{self.device_id}")
self.data = {}
# 添加调试信息
print(f"=== VirtualSeparator {self.device_id} is being created! ===")
print(f"=== Config: {self.config} ===")
print(f"=== Kwargs: {kwargs} ===")
# 从config或kwargs中获取配置参数
self.port = self.config.get("port") or kwargs.get("port", "VIRTUAL")
self._volume = self.config.get("volume") or kwargs.get("volume", 250.0)
self._has_phases = self.config.get("has_phases") or kwargs.get("has_phases", True)
# 处理其他kwargs参数但跳过已知的配置参数
skip_keys = {"port", "volume", "has_phases"}
for key, value in kwargs.items():
if key not in skip_keys and not hasattr(self, key):
setattr(self, key, value)
async def initialize(self) -> bool:
"""Initialize virtual separator"""
print(f"=== VirtualSeparator {self.device_id} initialize() called! ===")
self.logger.info(f"Initializing virtual separator {self.device_id}")
self.data.update(
{
"status": "Ready",
"separator_state": "Ready",
"volume": self._volume,
"has_phases": self._has_phases,
"phase_separation": False,
"stir_speed": 0.0,
"settling_time": 0.0,
"progress": 0.0,
"message": "",
}
)
return True
async def cleanup(self) -> bool:
"""Cleanup virtual separator"""
self.logger.info(f"Cleaning up virtual separator {self.device_id}")
return True
async def separate(
self,
purpose: str,
product_phase: str,
from_vessel: str,
separation_vessel: str,
to_vessel: str,
waste_phase_to_vessel: str = "",
solvent: str = "",
solvent_volume: float = 50.0,
through: str = "",
repeats: int = 1,
stir_time: float = 30.0,
stir_speed: float = 300.0,
settling_time: float = 300.0,
) -> bool:
"""Execute separate action - matches Separate action"""
self.logger.info(f"Separate: purpose={purpose}, product_phase={product_phase}, from_vessel={from_vessel}")
# 验证参数
if product_phase not in ["top", "bottom"]:
self.logger.error(f"Invalid product_phase {product_phase}, must be 'top' or 'bottom'")
self.data["message"] = f"产物相位 {product_phase} 无效,必须是 'top''bottom'"
return False
if purpose not in ["wash", "extract"]:
self.logger.error(f"Invalid purpose {purpose}, must be 'wash' or 'extract'")
self.data["message"] = f"分离目的 {purpose} 无效,必须是 'wash''extract'"
return False
# 开始分离
self.data.update(
{
"status": "Running",
"separator_state": "Separating",
"purpose": purpose,
"product_phase": product_phase,
"from_vessel": from_vessel,
"separation_vessel": separation_vessel,
"to_vessel": to_vessel,
"waste_phase_to_vessel": waste_phase_to_vessel,
"solvent": solvent,
"solvent_volume": solvent_volume,
"repeats": repeats,
"stir_speed": stir_speed,
"settling_time": settling_time,
"phase_separation": True,
"progress": 0.0,
"message": f"正在分离: {from_vessel} -> {to_vessel}",
}
)
# 模拟分离过程
total_time = (stir_time + settling_time) * repeats
simulation_time = min(total_time / 60.0, 15.0) # 最多模拟15秒
for repeat in range(repeats):
# 搅拌阶段
for progress in range(0, 51, 10):
await asyncio.sleep(simulation_time / (repeats * 10))
overall_progress = ((repeat * 100) + (progress * 0.5)) / repeats
self.data["progress"] = overall_progress
self.data["message"] = f"{repeat+1}次分离 - 搅拌中 ({progress}%)"
# 静置分相阶段
for progress in range(50, 101, 10):
await asyncio.sleep(simulation_time / (repeats * 10))
overall_progress = ((repeat * 100) + (progress * 0.5)) / repeats
self.data["progress"] = overall_progress
self.data["message"] = f"{repeat+1}次分离 - 静置分相中 ({progress}%)"
# 分离完成
self.data.update(
{
"status": "Ready",
"separator_state": "Ready",
"phase_separation": False,
"stir_speed": 0.0,
"progress": 100.0,
"message": f"分离完成: {repeats}次分离操作",
}
)
self.logger.info(f"Separation completed: {repeats} cycles from {from_vessel} to {to_vessel}")
return True
# 状态属性
@property
def status(self) -> str:
return self.data.get("status", "Unknown")
@property
def separator_state(self) -> str:
return self.data.get("separator_state", "Unknown")
@property
def volume(self) -> float:
return self.data.get("volume", self._volume)
@property
def has_phases(self) -> bool:
return self.data.get("has_phases", self._has_phases)
@property
def phase_separation(self) -> bool:
return self.data.get("phase_separation", False)
@property
def stir_speed(self) -> float:
return self.data.get("stir_speed", 0.0)
@property
def settling_time(self) -> float:
return self.data.get("settling_time", 0.0)
@property
def progress(self) -> float:
return self.data.get("progress", 0.0)
@property
def message(self) -> str:
return self.data.get("message", "")

View File

@@ -0,0 +1,151 @@
import time
from typing import Union
class VirtualSolenoidValve:
"""
虚拟电磁阀门 - 简单的开关型阀门,只有开启和关闭两个状态
"""
def __init__(self, port: str = "VIRTUAL", voltage: float = 12.0, response_time: float = 0.1):
self.port = port
self.voltage = voltage
self.response_time = response_time
# 状态属性
self._status = "Idle"
self._valve_state = "Closed" # "Open" or "Closed"
self._is_open = False
@property
def status(self) -> str:
return self._status
@property
def valve_state(self) -> str:
return self._valve_state
@property
def is_open(self) -> bool:
return self._is_open
def get_valve_position(self) -> str:
"""获取阀门位置状态"""
return "OPEN" if self._is_open else "CLOSED"
def set_valve_position(self, position: Union[str, bool]):
"""
设置阀门位置
Args:
position: "OPEN"/"CLOSED" 或 True/False
"""
self._status = "Busy"
# 模拟阀门响应时间
time.sleep(self.response_time)
if isinstance(position, str):
target_open = position.upper() == "OPEN"
elif isinstance(position, bool):
target_open = position
else:
self._status = "Error"
return "Error: Invalid position"
self._is_open = target_open
self._valve_state = "Open" if target_open else "Closed"
self._status = "Idle"
return f"Valve {'opened' if target_open else 'closed'}"
def open(self):
"""打开电磁阀"""
self._status = "Busy"
time.sleep(self.response_time)
self._is_open = True
self._valve_state = "Open"
self._status = "Idle"
return "Valve opened"
def close(self):
"""关闭电磁阀"""
self._status = "Busy"
time.sleep(self.response_time)
self._is_open = False
self._valve_state = "Closed"
self._status = "Idle"
return "Valve closed"
def set_state(self, command: Union[bool, str]):
"""
设置阀门状态 - 兼容 SendCmd 类型
Args:
command: True/False 或 "open"/"close"
"""
if isinstance(command, bool):
return self.open() if command else self.close()
elif isinstance(command, str):
if command.lower() in ["open", "on", "true", "1"]:
return self.open()
elif command.lower() in ["close", "closed", "off", "false", "0"]:
return self.close()
else:
self._status = "Error"
return "Error: Invalid command"
else:
self._status = "Error"
return "Error: Invalid command type"
def toggle(self):
"""切换阀门状态"""
if self._is_open:
return self.close()
else:
return self.open()
def is_closed(self) -> bool:
"""检查阀门是否关闭"""
return not self._is_open
def get_state(self) -> dict:
"""获取阀门完整状态"""
return {
"port": self.port,
"voltage": self.voltage,
"response_time": self.response_time,
"is_open": self._is_open,
"valve_state": self._valve_state,
"status": self._status,
"position": self.get_valve_position()
}
def reset(self):
"""重置阀门到关闭状态"""
return self.close()
def test_cycle(self, cycles: int = 3, delay: float = 1.0):
"""
测试阀门开关循环
Args:
cycles: 循环次数
delay: 每次开关间隔时间(秒)
"""
results = []
for i in range(cycles):
# 打开
result_open = self.open()
results.append(f"Cycle {i+1} - Open: {result_open}")
time.sleep(delay)
# 关闭
result_close = self.close()
results.append(f"Cycle {i+1} - Close: {result_close}")
time.sleep(delay)
return results

View File

@@ -1,149 +1,290 @@
import asyncio
import time
from enum import Enum
from typing import Union, Optional
import logging
from typing import Dict, Any, Optional
class VirtualTransferPump:
"""Virtual pump device specifically for Transfer protocol"""
class VirtualPumpMode(Enum):
Normal = 0
AccuratePos = 1
AccuratePosVel = 2
class VirtualPump:
"""虚拟泵类 - 模拟泵的基本功能,无需实际硬件"""
def __init__(self, device_id: str = None, config: Dict[str, Any] = None, **kwargs):
# 处理可能的不同调用方式
if device_id is None and 'id' in kwargs:
device_id = kwargs.pop('id')
if config is None and 'config' in kwargs:
config = kwargs.pop('config')
def __init__(self, device_id: str = None, max_volume: float = 25.0, mode: VirtualPumpMode = VirtualPumpMode.Normal, transfer_rate=0):
self.device_id = device_id or "virtual_pump"
self.max_volume = max_volume
self._transfer_rate = transfer_rate
self.mode = mode
# 设置默认值
self.device_id = device_id or "unknown_transfer_pump"
self.config = config or {}
# 状态变量
self._status = "Idle"
self._position = 0.0 # 当前柱塞位置 (ml)
self._max_velocity = 5.0 # 默认最大速度 (ml/s)
self._current_volume = 0.0 # 当前注射器中的体积
self.logger = logging.getLogger(f"VirtualPump.{self.device_id}")
self.logger = logging.getLogger(f"VirtualTransferPump.{self.device_id}")
self.data = {}
# 添加调试信息
print(f"=== VirtualTransferPump {self.device_id} is being created! ===")
print(f"=== Config: {self.config} ===")
print(f"=== Kwargs: {kwargs} ===")
# 从config或kwargs中获取配置参数
self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL')
self._max_volume = self.config.get('max_volume') or kwargs.get('max_volume', 50.0)
self._transfer_rate = self.config.get('transfer_rate') or kwargs.get('transfer_rate', 5.0)
self._current_volume = 0.0
self.is_running = False
async def initialize(self) -> bool:
"""Initialize virtual transfer pump"""
print(f"=== VirtualTransferPump {self.device_id} initialize() called! ===")
self.logger.info(f"Initializing virtual transfer pump {self.device_id}")
self.data.update({
"status": "Idle",
"current_volume": 0.0,
"max_volume": self._max_volume,
"transfer_rate": self._transfer_rate,
"from_vessel": "",
"to_vessel": "",
"progress": 0.0,
"transferred_volume": 0.0,
"current_status": "Ready"
})
"""初始化虚拟泵"""
self.logger.info(f"Initializing virtual pump {self.device_id}")
self._status = "Idle"
self._position = 0.0
self._current_volume = 0.0
return True
async def cleanup(self) -> bool:
"""Cleanup virtual transfer pump"""
self.logger.info(f"Cleaning up virtual transfer pump {self.device_id}")
"""清理虚拟泵"""
self.logger.info(f"Cleaning up virtual pump {self.device_id}")
self._status = "Idle"
return True
async def transfer(self, from_vessel: str, to_vessel: str, volume: float,
amount: str = "", time: float = 0, viscous: bool = False,
rinsing_solvent: str = "", rinsing_volume: float = 0.0,
rinsing_repeats: int = 0, solid: bool = False) -> bool:
"""Execute liquid transfer - matches Transfer action"""
self.logger.info(f"Transfer: {volume}mL from {from_vessel} to {to_vessel}")
# 计算转移时间
if time > 0:
transfer_time = time
else:
# 如果是粘性液体,降低转移速率
rate = self._transfer_rate * 0.5 if viscous else self._transfer_rate
transfer_time = volume / rate
self.data.update({
"status": "Running",
"from_vessel": from_vessel,
"to_vessel": to_vessel,
"current_status": "Transferring",
"progress": 0.0,
"transferred_volume": 0.0
})
# 模拟转移过程
steps = 10
step_time = transfer_time / steps
step_volume = volume / steps
for i in range(steps):
await asyncio.sleep(step_time)
progress = (i + 1) / steps * 100
transferred = (i + 1) * step_volume
self.data.update({
"progress": progress,
"transferred_volume": transferred,
"current_status": f"Transferring {progress:.1f}%"
})
self.logger.info(f"Transfer progress: {progress:.1f}% ({transferred:.1f}/{volume}mL)")
# 如果需要冲洗
if rinsing_solvent and rinsing_volume > 0 and rinsing_repeats > 0:
self.data["current_status"] = "Rinsing"
for repeat in range(rinsing_repeats):
self.logger.info(f"Rinsing cycle {repeat + 1}/{rinsing_repeats} with {rinsing_solvent}")
await asyncio.sleep(1) # 模拟冲洗时间
self.data.update({
"status": "Idle",
"current_status": "Transfer completed",
"progress": 100.0,
"transferred_volume": volume
})
return True
# 添加所有在virtual_device.yaml中定义的状态属性
# 基本属性
@property
def status(self) -> str:
return self.data.get("status", "Unknown")
return self._status
@property
def position(self) -> float:
"""当前柱塞位置 (ml)"""
return self._position
@property
def current_volume(self) -> float:
return self.data.get("current_volume", 0.0)
"""当前注射器中的体积 (ml)"""
return self._current_volume
@property
def max_volume(self) -> float:
return self.data.get("max_volume", self._max_volume)
def max_velocity(self) -> float:
return self._max_velocity
@property
def transfer_rate(self) -> float:
return self.data.get("transfer_rate", self._transfer_rate)
return self._transfer_rate
def set_max_velocity(self, velocity: float):
"""设置最大速度 (ml/s)"""
self._max_velocity = max(0.1, min(50.0, velocity)) # 限制在合理范围内
self.logger.info(f"Set max velocity to {self._max_velocity} ml/s")
@property
def from_vessel(self) -> str:
return self.data.get("from_vessel", "")
def get_status(self) -> str:
"""获取泵状态"""
return self._status
@property
def to_vessel(self) -> str:
return self.data.get("to_vessel", "")
async def _simulate_operation(self, duration: float):
"""模拟操作延时"""
self._status = "Busy"
await asyncio.sleep(duration)
self._status = "Idle"
@property
def progress(self) -> float:
return self.data.get("progress", 0.0)
def _calculate_duration(self, volume: float, velocity: float = None) -> float:
"""计算操作持续时间"""
if velocity is None:
velocity = self._max_velocity
return abs(volume) / velocity
@property
def transferred_volume(self) -> float:
return self.data.get("transferred_volume", 0.0)
# 基本泵操作
async def set_position(self, position: float, velocity: float = None):
"""
移动到绝对位置
Args:
position (float): 目标位置 (ml)
velocity (float): 移动速度 (ml/s)
"""
position = max(0, min(self.max_volume, position)) # 限制在有效范围内
volume_to_move = abs(position - self._position)
duration = self._calculate_duration(volume_to_move, velocity)
self.logger.info(f"Moving to position {position} ml (current: {self._position} ml)")
# 模拟移动过程
await self._simulate_operation(duration)
self._position = position
self._current_volume = position # 假设位置等于体积
self.logger.info(f"Reached position {self._position} ml")
@property
def current_status(self) -> str:
return self.data.get("current_status", "Ready")
async def pull_plunger(self, volume: float, velocity: float = None):
"""
拉取柱塞(吸液)
Args:
volume (float): 要拉取的体积 (ml)
velocity (float): 拉取速度 (ml/s)
"""
new_position = min(self.max_volume, self._position + volume)
actual_volume = new_position - self._position
if actual_volume <= 0:
self.logger.warning("Cannot pull - already at maximum volume")
return
duration = self._calculate_duration(actual_volume, velocity)
self.logger.info(f"Pulling {actual_volume} ml (from {self._position} to {new_position})")
await self._simulate_operation(duration)
self._position = new_position
self._current_volume = new_position
self.logger.info(f"Pulled {actual_volume} ml, current volume: {self._current_volume} ml")
async def push_plunger(self, volume: float, velocity: float = None):
"""
推出柱塞(排液)
Args:
volume (float): 要推出的体积 (ml)
velocity (float): 推出速度 (ml/s)
"""
new_position = max(0, self._position - volume)
actual_volume = self._position - new_position
if actual_volume <= 0:
self.logger.warning("Cannot push - already at minimum volume")
return
duration = self._calculate_duration(actual_volume, velocity)
self.logger.info(f"Pushing {actual_volume} ml (from {self._position} to {new_position})")
await self._simulate_operation(duration)
self._position = new_position
self._current_volume = new_position
self.logger.info(f"Pushed {actual_volume} ml, current volume: {self._current_volume} ml")
# 便捷操作方法
async def aspirate(self, volume: float, velocity: float = None):
"""
吸液操作
Args:
volume (float): 吸液体积 (ml)
velocity (float): 吸液速度 (ml/s)
"""
await self.pull_plunger(volume, velocity)
async def dispense(self, volume: float, velocity: float = None):
"""
排液操作
Args:
volume (float): 排液体积 (ml)
velocity (float): 排液速度 (ml/s)
"""
await self.push_plunger(volume, velocity)
async def transfer(self, volume: float, aspirate_velocity: float = None, dispense_velocity: float = None):
"""
转移操作(先吸后排)
Args:
volume (float): 转移体积 (ml)
aspirate_velocity (float): 吸液速度 (ml/s)
dispense_velocity (float): 排液速度 (ml/s)
"""
# 吸液
await self.aspirate(volume, aspirate_velocity)
# 短暂停顿
await asyncio.sleep(0.1)
# 排液
await self.dispense(volume, dispense_velocity)
async def empty_syringe(self, velocity: float = None):
"""清空注射器"""
await self.set_position(0, velocity)
async def fill_syringe(self, velocity: float = None):
"""充满注射器"""
await self.set_position(self.max_volume, velocity)
async def stop_operation(self):
"""停止当前操作"""
self._status = "Idle"
self.logger.info("Operation stopped")
# 状态查询方法
def get_position(self) -> float:
"""获取当前位置"""
return self._position
def get_current_volume(self) -> float:
"""获取当前体积"""
return self._current_volume
def get_remaining_capacity(self) -> float:
"""获取剩余容量"""
return self.max_volume - self._current_volume
def is_empty(self) -> bool:
"""检查是否为空"""
return self._current_volume <= 0.01 # 允许小量误差
def is_full(self) -> bool:
"""检查是否已满"""
return self._current_volume >= (self.max_volume - 0.01) # 允许小量误差
# 调试和状态信息
def get_pump_info(self) -> dict:
"""获取泵的详细信息"""
return {
"device_id": self.device_id,
"status": self._status,
"position": self._position,
"current_volume": self._current_volume,
"max_volume": self.max_volume,
"max_velocity": self._max_velocity,
"mode": self.mode.name,
"is_empty": self.is_empty(),
"is_full": self.is_full(),
"remaining_capacity": self.get_remaining_capacity()
}
def __str__(self):
return f"VirtualPump({self.device_id}: {self._current_volume:.2f}/{self.max_volume} ml, {self._status})"
def __repr__(self):
return self.__str__()
# 使用示例
async def demo():
"""虚拟泵使用示例"""
pump = VirtualPump("demo_pump", max_volume=50.0)
await pump.initialize()
print(f"Initial state: {pump}")
# 吸液测试
await pump.aspirate(10.0, velocity=2.0)
print(f"After aspirating 10ml: {pump}")
# 排液测试
await pump.dispense(5.0, velocity=3.0)
print(f"After dispensing 5ml: {pump}")
# 转移测试
await pump.transfer(3.0)
print(f"After transfer 3ml: {pump}")
# 清空测试
await pump.empty_syringe()
print(f"After emptying: {pump}")
print("\nPump info:", pump.get_pump_info())
if __name__ == "__main__":
asyncio.run(demo())

View File

@@ -0,0 +1,47 @@
import asyncio
import time
from typing import Dict, Any, Optional
class VirtualVacuumPump:
"""Virtual vacuum pump for testing"""
def __init__(self, device_id: Optional[str] = None, config: Optional[Dict[str, Any]] = None, **kwargs):
self.device_id = device_id or "unknown_vacuum_pump"
self.config = config or {}
self.data = {}
self._status = "OPEN"
async def initialize(self) -> bool:
"""Initialize virtual vacuum pump"""
self.data.update({
"status": self._status
})
return True
async def cleanup(self) -> bool:
"""Cleanup virtual vacuum pump"""
return True
@property
def status(self) -> str:
return self._status
def get_status(self) -> str:
return self._status
def set_status(self, string):
self._status = string
time.sleep(5)
def open(self):
self._status = "OPEN"
def close(self):
self._status = "CLOSED"
def is_open(self):
return self._status
def is_closed(self):
return not self._status

View File

@@ -1,7 +1,7 @@
io_snrd:
description: IO Board with 16 IOs
class:
module: unilabos.device_comms.SRND_16_IO:SRND_16_IO
module: ilabos.device_comms.SRND_16_IO:SRND_16_IO
type: python
hardware_interface:
name: modbus_client

View File

@@ -89,7 +89,7 @@ mock_filter:
target_volume: Float64
action_value_mappings:
filter:
type: ProtocolFilter
type: Filter
goal:
vessel: vessel
filtrate_vessel: filtrate_vessel
@@ -737,7 +737,7 @@ mock_stirrer_new:
max_stir_speed: Float64
action_value_mappings:
start_stir:
type: ProtocolStartStir
type: StartStir
goal:
vessel: vessel
stir_speed: stir_speed
@@ -760,7 +760,7 @@ mock_stirrer_new:
result:
success: success
stop_stir:
type: ProtocolStopStir
type: StopStir
goal:
vessel: vessel
feedback:

View File

@@ -48,14 +48,16 @@ solenoid_valve.mock:
feedback: {}
result: {}
handles:
input:
- handler_key: fluid-input
label: Fluid Input
- handler_key: in
label: in
io_type: target
data_type: fluid
output:
- handler_key: fluid-output
label: Fluid Output
side: NORTH
- handler_key: out
label: out
io_type: source
data_type: fluid
side: SOUTH
init_param_schema:
type: object
properties:
@@ -71,3 +73,13 @@ solenoid_valve:
class:
module: unilabos.devices.pump_and_valve.solenoid_valve:SolenoidValve
type: python
status_types:
status: String
valve_position: String
action_value_mappings:
set_valve_position:
type: StrSingleInput
goal:
string: position
feedback: {}
result: {}

View File

@@ -23,20 +23,12 @@ vacuum_pump.mock:
feedback: {}
result: {}
handles:
input:
- handler_key: fluid-input
label: Fluid Input
- handler_key: out
label: out
data_type: fluid
io_type: target
data_source: handle
data_key: fluid_in
output:
- handler_key: fluid-output
label: Fluid Output
data_type: fluid
io_type: source
data_source: executor
data_key: fluid_out
init_param_schema:
type: object
properties:
@@ -72,16 +64,8 @@ gas_source.mock:
feedback: {}
result: {}
handles:
input:
- handler_key: fluid-input
label: Fluid Input
data_type: fluid
io_type: target
data_source: handle
data_key: fluid_in
output:
- handler_key: fluid-output
label: Fluid Output
- handler_key: out
label: out
data_type: fluid
io_type: source
data_source: executor

View File

@@ -1,12 +1,82 @@
# 虚拟设备清单及连接特性
# 1. virtual_pump - 虚拟泵
# 描述具有多通道阀门特性的泵根据valve_position可连接多个容器
# 连接特性1个输入口 + 1个输出口当前配置实际应该有多个输出口
# 数据类型fluid流体连接
# 2. virtual_stirrer - 虚拟搅拌器
# 描述:机械连接设备,提供搅拌功能
# 连接特性1个双向连接点undirected
# 数据类型mechanical机械连接
# 3a. virtual_valve - 虚拟八通阀门
# 描述8通阀门实际配置为7通可切换流向
# 连接特性1个口连接注射泵 + 7个输出口
# 数据类型fluid流体连接
# 3b. virtual_solenoid_valve (电磁阀门)
# 描述:简单的开关型电磁阀,只有开启和关闭两个状态
# 连接特性1个输入口 + 1个输出口控制通断
# 数据类型fluid流体连接
# 4. virtual_centrifuge - 虚拟离心机
# 描述:单个样品处理设备,原地处理样品
# 连接特性1个输入口 + 1个输出口
# 数据类型resource资源/样品连接)
# 5. virtual_filter - 虚拟过滤器
# 描述:分离设备,将样品分离为滤液和滤渣
# 连接特性1个输入口 + 2个输出口滤液和滤渣
# 数据类型resource资源/样品连接)
# 6. virtual_heatchill - 虚拟加热/冷却器
# 描述:温控设备,容器直接放置在设备上进行温度控制
# 连接特性1个双向连接点undirected
# 数据类型mechanical机械/物理接触连接)
# 7. virtual_transfer_pump - 虚拟转移泵(注射器式)
# 描述:注射器式转移泵,通过同一个口吸入和排出液体
# 连接特性1个双向连接点undirected
# 数据类型fluid流体连接
# 8. virtual_column - 虚拟色谱柱
# 描述:分离纯化设备,用于样品纯化
# 连接特性1个输入口 + 1个输出口
# 数据类型resource资源/样品连接)
# 9. virtual_rotavap - 虚拟旋转蒸发仪
# 描述:旋转蒸发仪用于溶剂蒸发和浓缩,具有加热、旋转和真空功能
# 连接特性1个输入口样品1个输出口浓缩物1个冷凝器出口回收溶剂
# 数据类型resource资源/样品连接)
# 10. virtual_separator - 虚拟分液器
# 描述:分液器用于两相液体的分离,可进行萃取和洗涤操作
# 连接特性1个输入口混合液2个输出口上相和下相
# 数据类型fluid流体连接
# 11. virtual_vacuum_pump - 虚拟真空泵
# 描述:真空泵设备,用于抽真空操作和真空/充气循环
# 连接特性1个输入口连接需要抽真空的系统
# 数据类型fluid流体连接
# 主要功能:开启/关闭、状态控制ON/OFF
# 12. virtual_gas_source - 虚拟气源
# 描述:气源设备,用于充气操作和真空/充气循环
# 连接特性1个输出口向系统提供加压气体
# 数据类型fluid流体连接
# 主要功能:开启/关闭、状态控制ON/OFF
virtual_pump:
description: Virtual Pump for PumpTransferProtocol Testing
#icon: 这个注册的设备应该是写错了,后续删掉
class:
module: unilabos.devices.virtual.virtual_pump:VirtualPump
type: python
status_types:
status: String
position: Float64
valve_position: Int32 # 修复:使用 Int32 而不是 String
valve_position: Int32 # 修复:使用 Int32 而不是 String
max_volume: Float64
current_volume: Float64
action_value_mappings:
@@ -30,11 +100,20 @@ virtual_pump:
set_valve_position:
type: FloatSingleInput
goal:
Int32: Int32
float_in: valve_position
feedback:
status: status
result:
success: success
# 虚拟泵节点配置 - 具有多通道阀门特性根据valve_position可连接多个容器
handles:
- handler_key: pumpio
label: pumpio
data_type: fluid
io_type: source
data_source: handle
data_key: fluid_in
description: "泵的进液口,连接源容器"
schema:
type: object
properties:
@@ -48,6 +127,7 @@ virtual_pump:
virtual_stirrer:
description: Virtual Stirrer for StirProtocol Testing
icon: Stirrer.webp
class:
module: unilabos.devices.virtual.virtual_stirrer:VirtualStirrer
type: python
@@ -65,7 +145,7 @@ virtual_stirrer:
result:
success: success
start_stir:
type: ProtocolStartStir
type: StartStir
goal:
vessel: vessel
stir_speed: stir_speed
@@ -75,13 +155,23 @@ virtual_stirrer:
result:
success: success
stop_stir:
type: ProtocolStopStir
type: StopStir
goal:
vessel: vessel
feedback:
status: status
result:
success: success
# 虚拟搅拌器节点配置 - 机械连接设备,单一双向连接点
handles:
- handler_key: stirrer
label: stirrer
data_type: mechanical
side: NORTH
io_type: source
data_source: handle
data_key: vessel
description: "搅拌器的机械连接口,直接与反应容器连接提供搅拌功能"
schema:
type: object
properties:
@@ -96,10 +186,11 @@ virtual_stirrer:
default: 1000.0
additionalProperties: false
virtual_valve:
description: Virtual Valve for AddProtocol Testing
virtual_multiway_valve:
description: Virtual 8-Way Valve for flow direction control
icon: EightPipeline.webp
class:
module: unilabos.devices.virtual.virtual_valve:VirtualValve
module: unilabos.devices.virtual.virtual_multiway_valve:VirtualMultiwayValve
type: python
status_types:
status: String
@@ -115,18 +206,80 @@ virtual_valve:
feedback: {}
result:
success: success
open:
type: EmptyIn
goal: {}
feedback: {}
result:
success: success
close:
type: EmptyIn
goal: {}
feedback: {}
result:
success: success
# 八通阀门节点配置 - 1个输入口8个输出口可切换流向
handles:
- handler_key: transferpump
label: transferpump
data_type: fluid
side: NORTH
io_type: target
data_source: handle
data_key: fluid_in
description: "八通阀门进液口,接收来源流体"
- handler_key: 1
label: 1
data_type: fluid
side: NORTH
io_type: source
data_source: executor
data_key: fluid_port_1
description: "八通阀门端口1position=1时流体从此口流出"
- handler_key: 2
label: 2
data_type: fluid
side: EAST
io_type: source
data_source: executor
data_key: fluid_port_2
description: "八通阀门端口2position=2时流体从此口流出"
- handler_key: 3
label: 3
data_type: fluid
side: EAST
io_type: source
data_source: executor
data_key: fluid_port_3
description: "八通阀门端口3position=3时流体从此口流出"
- handler_key: 4
label: 4
data_type: fluid
side: SOUTH
io_type: source
data_source: executor
data_key: fluid_port_4
description: "八通阀门端口4position=4时流体从此口流出"
- handler_key: 5
label: 5
data_type: fluid
side: SOUTH
io_type: source
data_source: executor
data_key: fluid_port_5
description: "八通阀门端口5position=5时流体从此口流出"
- handler_key: 7
label: 7
data_type: fluid
side: WEST
io_type: source
data_source: executor
data_key: fluid_port_7
description: "八通阀门端口7position=7时流体从此口流出"
- handler_key: 6
label: 6
data_type: fluid
side: WEST
io_type: source
data_source: executor
data_key: fluid_port_6
description: "八通阀门端口6position=6时流体从此口流出"
- handler_key: 8
label: 8
data_type: fluid
side: NORTH
io_type: source
data_source: executor
data_key: fluid_port_8
description: "八通阀门端口8position=8时流体从此口流出"
schema:
type: object
properties:
@@ -135,11 +288,74 @@ virtual_valve:
default: "VIRTUAL"
positions:
type: integer
default: 6
default: 8
additionalProperties: false
virtual_solenoid_valve:
description: Virtual Solenoid Valve for simple on/off flow control
#icon: SolenoidValve.webp暂时还没有
class:
module: unilabos.devices.virtual.virtual_solenoid_valve:VirtualSolenoidValve
type: python
status_types:
status: String
valve_state: String # "open" or "closed"
is_open: Bool
action_value_mappings:
open:
type: SendCmd
goal:
command: "open"
feedback: {}
result:
success: success
close:
type: SendCmd
goal:
command: "close"
feedback: {}
result:
success: success
set_state:
type: SendCmd
goal:
command: command
feedback: {}
result:
success: success
# 电磁阀门节点配置 - 双向流通的开关型阀门,流动方向由泵决定
handles:
- handler_key: in
label: in
data_type: fluid
side: NORTH
io_type: target
data_source: handle
data_key: fluid_port_in
description: "电磁阀的双向流体口,开启时允许流体双向通过,关闭时完全阻断"
- handler_key: out
label: out
data_type: fluid
side: SOUTH
io_type: source
data_source: handle
data_key: fluid_port_out
description: "电磁阀的双向流体口,开启时允许流体双向通过,关闭时完全阻断"
schema:
type: object
properties:
port:
type: string
default: "VIRTUAL"
voltage:
type: number
default: 12.0
response_time:
type: number
default: 0.1
additionalProperties: false
virtual_centrifuge:
description: Virtual Centrifuge for CentrifugeProtocol Testing
#icon: Centrifuge.webp暂时还没有
class:
module: unilabos.devices.virtual.virtual_centrifuge:VirtualCentrifuge
type: python
@@ -156,7 +372,7 @@ virtual_centrifuge:
time_remaining: Float64
action_value_mappings:
centrifuge:
type: ProtocolCentrifuge
type: Centrifuge
goal:
vessel: vessel
speed: speed
@@ -170,6 +386,16 @@ virtual_centrifuge:
result:
success: success
message: message
# 虚拟离心机节点配置 - 单个样品处理设备,输入输出都是同一个样品容器
handles:
- handler_key: centrifuge
label: centrifuge
data_type: transport
side: NORTH
io_type: target
data_source: handle
data_key: vessel
description: "需要离心的样品容器"
schema:
type: object
properties:
@@ -189,6 +415,7 @@ virtual_centrifuge:
virtual_filter:
description: Virtual Filter for FilterProtocol Testing
icon: Filter.webp
class:
module: unilabos.devices.virtual.virtual_filter:VirtualFilter
type: python
@@ -205,7 +432,7 @@ virtual_filter:
message: String
action_value_mappings:
filter_sample:
type: ProtocolFilter
type: Filter
goal:
vessel: vessel
filtrate_vessel: filtrate_vessel
@@ -222,6 +449,32 @@ virtual_filter:
result:
success: success
message: message
# 虚拟过滤器节点配置 - 分离设备1个输入(原始样品)2个输出(滤液和滤渣)
handles:
- handler_key: filterin
label: filterin
data_type: fluid
side: NORTH
io_type: target
data_source: handle
data_key: vessel
description: "需要过滤的原始样品容器"
- handler_key: filtrate_out
label: filtrate_out
data_type: fluid
side: SOUTH
io_type: source
data_source: executor
data_key: filtrate_vessel
description: "过滤后的滤液容器"
- handler_key: filter-residue-out
label: Residue
data_type: resource
side: WEST
io_type: source
data_source: executor
data_key: residue_vessel
description: "过滤后的滤渣(固体残留物)"
schema:
type: object
properties:
@@ -238,6 +491,7 @@ virtual_filter:
virtual_heatchill:
description: Virtual HeatChill for HeatChillProtocol Testing
icon: Heater.webp
class:
module: unilabos.devices.virtual.virtual_heatchill:VirtualHeatChill
type: python
@@ -275,6 +529,16 @@ virtual_heatchill:
status: status
result:
success: success
# 虚拟加热/冷却器节点配置 - 温控设备,单一双向连接点用于放置容器
handles:
- handler_key: heatchill
label: heatchill
data_type: mechanical
side: NORTH
io_type: source
data_source: handle
data_key: vessel
description: "加热/冷却器的物理连接口,容器直接放置在设备上进行温度控制"
schema:
type: object
properties:
@@ -286,30 +550,26 @@ virtual_heatchill:
default: 200.0
min_temp:
type: number
default: -80.0
default: -80
max_stir_speed:
type: number
default: 1000.0
additionalProperties: false
virtual_transfer_pump:
description: Virtual Transfer Pump for TransferProtocol Testing
description: Virtual Transfer Pump for TransferProtocol Testing (Syringe-style)
icon: Pump.webp
class:
module: unilabos.devices.virtual.virtual_transferpump:VirtualTransferPump
module: unilabos.devices.virtual.virtual_transferpump:VirtualPump
type: python
status_types:
status: String
current_volume: Float64
max_volume: Float64
transfer_rate: Float64
from_vessel: String
to_vessel: String
progress: Float64
transferred_volume: Float64
current_status: String
action_value_mappings:
transfer:
type: ProtocolTransfer
type: Transfer
goal:
from_vessel: from_vessel
to_vessel: to_vessel
@@ -328,6 +588,16 @@ virtual_transfer_pump:
result:
success: success
message: message
# 注射器式转移泵节点配置 - 只有一个双向连接口,可吸入和排出液体
handles:
- handler_key: transferpump
label: transferpump
data_type: fluid
side: SOUTH
io_type: source
data_source: handle
data_key: fluid_port
description: "注射器式转移泵的唯一连接口,通过阀门切换实现吸入和排出"
schema:
type: object
properties:
@@ -344,6 +614,7 @@ virtual_transfer_pump:
virtual_column:
description: Virtual Column for RunColumn Protocol Testing
#icon: Column.webp暂时还没有
class:
module: unilabos.devices.virtual.virtual_column:VirtualColumn
type: python
@@ -359,7 +630,7 @@ virtual_column:
current_status: String
action_value_mappings:
run_column:
type: ProtocolRunColumn
type: RunColumn
goal:
from_vessel: from_vessel
to_vessel: to_vessel
@@ -370,6 +641,24 @@ virtual_column:
result:
success: success
message: message
# 虚拟色谱柱节点配置 - 分离纯化设备1个样品输入口1个纯化产物输出口
handles:
- handler_key: columnin
label: columnin
data_type: fluid
side: NORTH
io_type: target
data_source: handle
data_key: from_vessel
description: "需要纯化的样品输入口"
- handler_key: columnout
label: columnout
data_type: fluid
side: SOUTH
io_type: source
data_source: executor
data_key: to_vessel
description: "经过色谱柱纯化的产物输出口"
schema:
type: object
properties:
@@ -385,4 +674,238 @@ virtual_column:
column_diameter:
type: number
default: 2.0
additionalProperties: false
additionalProperties: false
virtual_rotavap:
description: Virtual Rotary Evaporator for EvaporateProtocol Testing
icon: Rotaryevaporator.webp
class:
module: unilabos.devices.virtual.virtual_rotavap:VirtualRotavap
type: python
status_types:
status: String
rotavap_state: String
current_temp: Float64
target_temp: Float64
max_temp: Float64
rotation_speed: Float64
max_rotation_speed: Float64
vacuum_pressure: Float64
evaporated_volume: Float64
progress: Float64
message: String
action_value_mappings:
evaporate:
type: Evaporate
goal:
vessel: vessel
pressure: pressure
temp: temp
time: time
stir_speed: stir_speed
feedback:
progress: progress
current_temp: current_temp
evaporated_volume: evaporated_volume
current_status: status
result:
success: success
message: message
# 虚拟旋转蒸发仪节点配置 - 1个双向口(样品进出)1个单向输出口(冷凝溶剂)
handles:
- handler_key: rotavap-sample
label: rotavap-sample
data_type: fluid
side: NORTH
io_type: target
data_source: handle
data_key: vessel
description: "样品的双向连接口,可放入需要蒸发的样品,蒸发完成后取出浓缩物"
- handler_key: rotavap-distillate-outlet
label: Distillate Outlet
data_type: fluid
side: WEST
io_type: source
data_source: executor
data_key: distillate_vessel
description: "冷凝回收的溶剂单向输出口,连接收集瓶"
schema:
type: object
properties:
port:
type: string
default: "VIRTUAL"
max_temp:
type: number
default: 180.0
max_rotation_speed:
type: number
default: 280.0
additionalProperties: false
virtual_separator:
description: Virtual Separator for SeparateProtocol Testing
icon: Separator.webp
class:
module: unilabos.devices.virtual.virtual_separator:VirtualSeparator
type: python
status_types:
status: String
separator_state: String
volume: Float64
has_phases: Bool
phase_separation: Bool
stir_speed: Float64
settling_time: Float64
progress: Float64
message: String
action_value_mappings:
separate:
type: Separate
goal:
purpose: purpose
product_phase: product_phase
from_vessel: from_vessel
separation_vessel: separation_vessel
to_vessel: to_vessel
waste_phase_to_vessel: waste_phase_to_vessel
solvent: solvent
solvent_volume: solvent_volume
through: through
repeats: repeats
stir_time: stir_time
stir_speed: stir_speed
settling_time: settling_time
feedback:
progress: progress
current_status: status
result:
success: success
message: message
# 虚拟分液器节点配置 - 分离设备1个输入口(混合液)2个输出口(上相和下相)
handles:
- handler_key: separatorin
label: separatorin
data_type: fluid
side: NORTH
io_type: target
data_source: handle
data_key: from_vessel
description: "需要分离的混合液体输入口"
- handler_key: separatorout
label: separatorout
data_type: fluid
side: SOUTH
io_type: source
data_source: executor
data_key: bottom_outlet
description: "下相(重相)液体输出口"
schema:
type: object
properties:
port:
type: string
default: "VIRTUAL"
volume:
type: number
default: 250.0
has_phases:
type: boolean
default: true
additionalProperties: false
virtual_vacuum_pump:
description: Virtual vacuum pump
icon: Vacuum.webp
class:
module: unilabos.devices.virtual.virtual_vacuum_pump:VirtualVacuumPump
type: python
status_types:
status: String
action_value_mappings:
open:
type: EmptyIn
goal: {}
feedback: {}
result: {}
close:
type: EmptyIn
goal: {}
feedback: {}
result: {}
set_status:
type: StrSingleInput
goal:
string: string
feedback: {}
result: {}
# 虚拟真空泵节点配置 - 真空设备1个输入口连接需要抽真空的系统
handles:
- handler_key: vacuumpump
label: vacuumpump
data_type: fluid
side: SOUTH
io_type: source
data_source: handle
data_key: fluid_in
description: "真空泵进气口,连接需要抽真空的容器或管路"
schema:
type: object
properties:
port:
type: string
default: "VIRTUAL"
description: "通信端口"
additionalProperties: false
virtual_gas_source:
description: Virtual gas source
#icon: GasSource.webp暂时还没有
class:
module: unilabos.devices.virtual.virtual_gas_source:VirtualGasSource
type: python
status_types:
status: String
action_value_mappings:
open:
type: EmptyIn
goal: {}
feedback: {}
result: {}
close:
type: EmptyIn
goal: {}
feedback: {}
result: {}
set_status:
type: StrSingleInput
goal:
string: string
feedback: {}
result: {}
# 虚拟气源节点配置 - 气体供应设备1个输出口提供加压气体
handles:
- handler_key: gassource
label: gassource
data_type: fluid
side: SOUTH
io_type: source
data_source: executor
data_key: fluid_out
description: "气源出气口,向容器或管路提供加压气体"
schema:
type: object
properties:
port:
type: string
default: "VIRTUAL"
description: "通信端口"
gas_type:
type: string
default: "nitrogen"
description: "气体类型"
max_pressure:
type: number
default: 5.0
description: "最大输出压力 (bar)"
additionalProperties: false

View File

@@ -0,0 +1,21 @@
container:
description: regular organic container
class:
module: unilabos.resources.container:RegularContainer
type: unilabos
handles:
- handler_key: top
label: top
io_type: target
data_type: fluid
side: NORTH
- handler_key: bottom
label: bottom
io_type: source
data_type: fluid
side: SOUTH
- handler_key: bind
label: bind
io_type: target
data_type: mechanical
side: SOUTH

View File

@@ -0,0 +1,67 @@
import json
from unilabos_msgs.msg import Resource
from unilabos.ros.msgs.message_converter import convert_from_ros_msg
class RegularContainer(object):
# 第一个参数必须是id传入
# noinspection PyShadowingBuiltins
def __init__(self, id: str):
self.id = id
self.ulr_resource = Resource()
self._data = None
@property
def ulr_resource_data(self):
if self._data is None:
self._data = json.loads(self.ulr_resource.data) if self.ulr_resource.data else {}
return self._data
@ulr_resource_data.setter
def ulr_resource_data(self, value: dict):
self._data = value
self.ulr_resource.data = json.dumps(self._data)
@property
def liquid_type(self):
return self.ulr_resource_data.get("liquid_type", None)
@liquid_type.setter
def liquid_type(self, value: str):
if value is not None:
self.ulr_resource_data["liquid_type"] = value
else:
self.ulr_resource_data.pop("liquid_type", None)
@property
def liquid_volume(self):
return self.ulr_resource_data.get("liquid_volume", None)
@liquid_volume.setter
def liquid_volume(self, value: float):
if value is not None:
self.ulr_resource_data["liquid_volume"] = value
else:
self.ulr_resource_data.pop("liquid_volume", None)
def get_ulr_resource(self) -> Resource:
"""
获取UlrResource对象
:return: UlrResource对象
"""
self.ulr_resource_data = self.ulr_resource_data # 确保数据被更新
return self.ulr_resource
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,9 +1,13 @@
import importlib
import inspect
import json
from typing import Union
from typing import Union, Any
import numpy as np
import networkx as nx
from unilabos_msgs.msg import Resource
from unilabos.resources.container import RegularContainer
from unilabos.ros.msgs.message_converter import convert_from_ros_msg_with_mapping, convert_to_ros_msg
try:
from pylabrobot.resources.resource import Resource as ResourcePLR
@@ -80,6 +84,8 @@ def canonicalize_links_ports(data: dict) -> dict:
# 第一遍处理将字符串类型的port转换为字典格式
for link in data.get("links", []):
port = link.get("port")
if link["type"] == "physical":
link["type"] = "fluid"
if isinstance(port, int):
port = str(port)
if isinstance(port, str):
@@ -153,7 +159,27 @@ def read_node_link_json(json_file):
physical_setup_graph = nx.node_link_graph(data, multigraph=False) # edges="links" 3.6 warning
handle_communications(physical_setup_graph)
return physical_setup_graph
return physical_setup_graph, data
def modify_to_backend_format(data: list[dict[str, Any]]) -> list[dict[str, Any]]:
for edge in data:
port = edge.pop("port", {})
source = edge["source"]
target = edge["target"]
if source in port:
edge["sourceHandle"] = port[source]
elif "source_port" in edge:
edge["sourceHandle"] = edge.pop("source_port")
if target in port:
edge["targetHandle"] = port[target]
elif "target_port" in edge:
edge["targetHandle"] = edge.pop("target_port")
edge["id"] = f"reactflow__edge-{source}-{edge['sourceHandle']}-{target}-{edge['targetHandle']}"
for key in ["source_port", "target_port"]:
if key in edge:
edge.pop(key)
return data
def read_graphml(graphml_file):
@@ -178,7 +204,7 @@ def read_graphml(graphml_file):
physical_setup_graph = nx.node_link_graph(data, edges="links", multigraph=False) # edges="links" 3.6 warning
handle_communications(physical_setup_graph)
return physical_setup_graph
return physical_setup_graph, data
def dict_from_graph(graph: nx.Graph) -> dict:
@@ -466,6 +492,10 @@ def initialize_resource(resource_config: dict) -> list[dict]:
if resource_config.get("position") is not None:
r["position"] = resource_config["position"]
r = tree_to_list([r])
elif resource_class_config["type"] == "unilabos":
res_instance: RegularContainer = RESOURCE(id=resource_config["name"])
res_instance.ulr_resource = convert_to_ros_msg(Resource, {k:v for k,v in resource_config.items() if k != "class"})
r = [res_instance.get_ulr_resource_as_dict()]
elif isinstance(RESOURCE, dict):
r = [RESOURCE.copy()]

View File

@@ -45,6 +45,7 @@ def exit() -> None:
def main(
devices_config: Dict[str, Any] = {},
resources_config: list=[],
resources_edge_config: list=[],
graph: Optional[Dict[str, Any]] = None,
controllers_config: Dict[str, Any] = {},
bridges: List[Any] = [],
@@ -62,6 +63,7 @@ def main(
"host_node",
devices_config,
resources_config,
resources_edge_config,
graph,
controllers_config,
bridges,
@@ -97,6 +99,7 @@ def main(
def slave(
devices_config: Dict[str, Any] = {},
resources_config=[],
resources_edge_config=[],
graph: Optional[Dict[str, Any]] = None,
controllers_config: Dict[str, Any] = {},
bridges: List[Any] = [],

View File

@@ -100,7 +100,7 @@ _action_mapping: Dict[Type, Dict[str, Any]] = {
# 添加Protocol action类型到映射
for py_msgtype in imsg.__all__:
if py_msgtype not in _action_mapping and py_msgtype.endswith("Protocol"):
if py_msgtype not in _action_mapping and (py_msgtype.endswith("Protocol") or py_msgtype.startswith("Protocol")):
try:
protocol_class = msg_converter_manager.get_class(f"unilabos.messages.{py_msgtype}")
action_name = py_msgtype.replace("Protocol", "")
@@ -117,6 +117,7 @@ for py_msgtype in imsg.__all__:
"result": {k: k for k in action_type.Result().get_fields_and_field_types().keys()},
}
except Exception:
traceback.print_exc()
logger.debug(f"Failed to load Protocol class: {py_msgtype}")
# Python到ROS消息转换器

View File

@@ -19,6 +19,7 @@ from rclpy.service import Service
from unilabos_msgs.action import SendCmd
from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response
from unilabos.resources.container import RegularContainer
from unilabos.resources.graphio import (
convert_resources_to_type,
convert_resources_from_type,
@@ -344,6 +345,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
LIQUID_VOLUME = other_calling_param.pop("LIQUID_VOLUME", [])
LIQUID_INPUT_SLOT = other_calling_param.pop("LIQUID_INPUT_SLOT", [])
slot = other_calling_param.pop("slot", "-1")
resource = None
if slot != "-1": # slot为负数的时候采用assign方法
other_calling_param["slot"] = slot
# 本地拿到这个物料,可能需要先做初始化?
@@ -362,6 +364,28 @@ class BaseROS2DeviceNode(Node, Generic[T]):
if initialize_full:
resources = initialize_resources([resources])
request.resources = [convert_to_ros_msg(Resource, resources)]
if len(LIQUID_INPUT_SLOT) and LIQUID_INPUT_SLOT[0] == -1:
container_instance = request.resources[0]
container_query_dict: dict = resources
found_resources = self.resource_tracker.figure_resource({"id": container_query_dict["name"]}, try_mode=True)
if not len(found_resources):
self.resource_tracker.add_resource(container_instance)
logger.info(f"添加物料{container_query_dict['name']}到资源跟踪器")
else:
assert len(found_resources) == 1, f"找到多个同名物料: {container_query_dict['name']}, 请检查物料系统"
resource = found_resources[0]
if isinstance(resource, Resource):
regular_container = RegularContainer(resource.id)
regular_container.ulr_resource = resource
regular_container.ulr_resource_data.update(json.loads(container_instance.data))
logger.info(f"更新物料{container_query_dict['name']}的数据{resource.data} ULR")
elif isinstance(resource, dict):
if "data" not in resource:
resource["data"] = {}
resource["data"].update(json.loads(container_instance.data))
logger.info(f"更新物料{container_query_dict['name']}的数据{resource['data']} dict")
else:
logger.info(f"更新物料{container_query_dict['name']}出现不支持的数据类型{type(resource)} {resource}")
response = rclient.call(request)
# 应该先add_resource了
res.response = "OK"
@@ -385,7 +409,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
res.response = serialize_result_info(traceback.format_exc(), False, {})
return res
# 接下来该根据bind_parent_id进行assign了目前只有plr可以进行assign不然没有办法输入到物料系统中
resource = self.resource_tracker.figure_resource({"name": bind_parent_id})
if bind_parent_id != self.node_name:
resource = self.resource_tracker.figure_resource({"name": bind_parent_id}) # 拿到父节点进行具体assign等操作
# request.resources = [convert_to_ros_msg(Resource, resources)]
try:
@@ -435,7 +460,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
"bind_parent_id": bind_parent_id,
}
)
future = action_client.send_goal_async(goal, goal_uuid=uuid.uuid4())
future = action_client.send_goal_async(goal)
def done_cb(*args):
self.lab_logger().info(f"向meshmanager发送新增resource完成")
@@ -601,10 +626,10 @@ class BaseROS2DeviceNode(Node, Generic[T]):
goal = goal_handle.request
# 从目标消息中提取参数, 并调用对应的方法
if "sequence" in self._action_value_mappings:
if "sequence" in action_value_mapping:
# 如果一个指令对应函数的连续调用,如启动和等待结果,默认参数应该属于第一个函数调用
def ACTION(**kwargs):
for i, action in enumerate(self._action_value_mappings["sequence"]):
for i, action in enumerate(action_value_mapping["sequence"]):
if i == 0:
self.lab_logger().info(f"执行序列动作第一步: {action}")
self.get_real_function(self.driver_instance, action)[0](**kwargs)
@@ -612,9 +637,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
self.lab_logger().info(f"执行序列动作后续步骤: {action}")
self.get_real_function(self.driver_instance, action)[0]()
action_paramtypes = get_type_hints(
self.get_real_function(self.driver_instance, self._action_value_mappings["sequence"][0])
)[1]
action_paramtypes = self.get_real_function(self.driver_instance, action_value_mapping["sequence"][0])[1]
else:
ACTION, action_paramtypes = self.get_real_function(self.driver_instance, action_name)
@@ -903,9 +926,9 @@ class ROS2DeviceNode:
from unilabos.ros.nodes.presets.protocol_node import ROS2ProtocolNode
if self._driver_class is ROS2ProtocolNode:
self._driver_creator = ProtocolNodeCreator(driver_class, children=children)
self._driver_creator = ProtocolNodeCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
else:
self._driver_creator = DeviceClassCreator(driver_class)
self._driver_creator = DeviceClassCreator(driver_class, children=children, resource_tracker=self.resource_tracker)
if driver_is_ros:
driver_params["device_id"] = device_id

View File

@@ -22,6 +22,7 @@ from unilabos_msgs.srv import (
) # type: ignore
from unique_identifier_msgs.msg import UUID
from unilabos.config.config import BasicConfig
from unilabos.registry.registry import lab_registry
from unilabos.resources.graphio import initialize_resource
from unilabos.resources.registry import add_schema
@@ -58,6 +59,7 @@ class HostNode(BaseROS2DeviceNode):
device_id: str,
devices_config: Dict[str, Any],
resources_config: list,
resources_edge_config: list[dict],
physical_setup_graph: Optional[Dict[str, Any]] = None,
controllers_config: Optional[Dict[str, Any]] = None,
bridges: Optional[List[Any]] = None,
@@ -96,6 +98,7 @@ class HostNode(BaseROS2DeviceNode):
self.server_latest_timestamp = 0.0 #
self.devices_config = devices_config
self.resources_config = resources_config
self.resources_edge_config = resources_edge_config
self.physical_setup_graph = physical_setup_graph
if controllers_config is None:
controllers_config = {}
@@ -144,13 +147,15 @@ class HostNode(BaseROS2DeviceNode):
self.device_status = {} # 用来存储设备状态
self.device_status_timestamps = {} # 用来存储设备状态最后更新时间
if BasicConfig.upload_registry:
from unilabos.app.mq import mqtt_client
from unilabos.app.mq import mqtt_client
for device_info in lab_registry.obtain_registry_device_info():
mqtt_client.publish_registry(device_info["id"], device_info)
for resource_info in lab_registry.obtain_registry_resource_info():
mqtt_client.publish_registry(resource_info["id"], resource_info)
for device_info in lab_registry.obtain_registry_device_info():
mqtt_client.publish_registry(device_info["id"], device_info)
for resource_info in lab_registry.obtain_registry_resource_info():
mqtt_client.publish_registry(resource_info["id"], resource_info)
else:
self.lab_logger().warning("本次启动注册表不报送云端如果您需要联网调试请使用unilab-register命令进行单独报送或者在启动命令增加--upload_registry")
time.sleep(1) # 等待MQTT连接稳定
# 首次发现网络中的设备
self._discover_devices()
@@ -191,24 +196,36 @@ class HostNode(BaseROS2DeviceNode):
)
resource_with_parent_name = []
resource_ids_to_instance = {i["id"]: i for i in resources_config}
resource_name_to_with_parent_name = {}
for res in resources_config:
if res.get("parent") and res.get("type") == "device" and res.get("class"):
parent_id = res.get("parent")
parent_res = resource_ids_to_instance[parent_id]
if parent_res.get("type") == "device" and parent_res.get("class"):
resource_with_parent_name.append(copy.deepcopy(res))
resource_with_parent_name[-1]["id"] = f"{parent_res['id']}/{res['id']}"
continue
# if res.get("parent") and res.get("type") == "device" and res.get("class"):
# parent_id = res.get("parent")
# parent_res = resource_ids_to_instance[parent_id]
# if parent_res.get("type") == "device" and parent_res.get("class"):
# resource_with_parent_name.append(copy.deepcopy(res))
# resource_name_to_with_parent_name[resource_with_parent_name[-1]["id"]] = f"{parent_res['id']}/{res['id']}"
# resource_with_parent_name[-1]["id"] = f"{parent_res['id']}/{res['id']}"
# continue
resource_with_parent_name.append(copy.deepcopy(res))
# for edge in self.resources_edge_config:
# edge["source"] = resource_name_to_with_parent_name.get(edge.get("source"), edge.get("source"))
# edge["target"] = resource_name_to_with_parent_name.get(edge.get("target"), edge.get("target"))
try:
for bridge in self.bridges:
if hasattr(bridge, "resource_add"):
from unilabos.app.web.client import HTTPClient
client: HTTPClient = bridge
resource_start_time = time.time()
resource_add_res = bridge.resource_add(add_schema(resource_with_parent_name), True)
resource_add_res = client.resource_add(add_schema(resource_with_parent_name), False)
resource_end_time = time.time()
self.lab_logger().info(
f"[Host Node-Resource] 物料上传 {round(resource_end_time - resource_start_time, 5) * 1000} ms"
)
resource_add_res = client.resource_edge_add(self.resources_edge_config, False)
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"
)
except Exception as ex:
self.lab_logger().error("[Host Node-Resource] 添加物料出错!")
self.lab_logger().error(traceback.format_exc())
@@ -383,18 +400,24 @@ class HostNode(BaseROS2DeviceNode):
liquid_volume: list[int],
slot_on_deck: str,
):
init_new_res = initialize_resource(
{
"name": res_id,
"class": class_name,
"parent": parent,
"position": {
"x": bind_locations.x,
"y": bind_locations.y,
"z": bind_locations.z,
},
}
) # flatten的格式
res_creation_input = {
"name": res_id,
"class": class_name,
"parent": parent,
"position": {
"x": bind_locations.x,
"y": bind_locations.y,
"z": bind_locations.z,
},
}
if len(liquid_input_slot) and liquid_input_slot[0] == -1: # 目前container只逐个创建
res_creation_input.update({
"data": {
"liquid_type": liquid_type[0] if liquid_type else None,
"liquid_volume": liquid_volume[0] if liquid_volume else None,
}
})
init_new_res = initialize_resource(res_creation_input) # flatten的格式
resources = init_new_res # initialize_resource已经返回list[dict]
device_ids = [device_id]
bind_parent_id = [parent]
@@ -751,8 +774,10 @@ class HostNode(BaseROS2DeviceNode):
self.lab_logger().info(f"[Host Node-Resource] Add request received: {len(resources)} resources")
success = False
if len(self.bridges) > 0:
r = self.bridges[-1].resource_add(add_schema(resources))
if len(self.bridges) > 0: # 边的提交待定
from unilabos.app.web.client import HTTPClient
client: HTTPClient = self.bridges[-1]
r = client.resource_add(add_schema(resources), False)
success = bool(r)
response.success = success

View File

@@ -110,7 +110,8 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
def initialize_device(self, device_id, device_config):
"""初始化设备并创建相应的动作客户端"""
device_id_abs = f"{self.device_id}/{device_id}"
# device_id_abs = f"{self.device_id}/{device_id}"
device_id_abs = f"{device_id}"
self.lab_logger().info(f"初始化子设备: {device_id_abs}")
d = self.sub_devices[device_id] = initialize_device_from_dict(device_id_abs, device_config)
@@ -256,12 +257,12 @@ class ROS2ProtocolNode(BaseROS2DeviceNode):
return write_func(*args, **kwargs)
if read_method:
bound_read = MethodType(_read, device.driver_instance)
setattr(device.driver_instance, read_method, bound_read)
# bound_read = MethodType(_read, device.driver_instance)
setattr(device.driver_instance, read_method, _read)
if write_method:
bound_write = MethodType(_write, device.driver_instance)
setattr(device.driver_instance, write_method, bound_write)
# bound_write = MethodType(_write, device.driver_instance)
setattr(device.driver_instance, write_method, _write)
async def _update_resources(self, goal, protocol_kwargs):

View File

@@ -25,7 +25,7 @@ class DeviceNodeResourceTracker(object):
def clear_resource(self):
self.resources = []
def figure_resource(self, query_resource):
def figure_resource(self, query_resource, try_mode=False):
if isinstance(query_resource, list):
return [self.figure_resource(r) for r in query_resource]
res_id = query_resource.id if hasattr(query_resource, "id") else (query_resource.get("id") if isinstance(query_resource, dict) else None)
@@ -45,10 +45,14 @@ class DeviceNodeResourceTracker(object):
res_list.extend(
self.loop_find_resource(r, resource_cls_type, identifier_key, getattr(query_resource, identifier_key))
)
assert len(res_list) == 1, f"{query_resource} 找到多个资源,请检查资源是否唯一: {res_list}"
if not try_mode:
assert len(res_list) > 0, f"没有找到资源 {query_resource},请检查资源是否存在"
assert len(res_list) == 1, f"{query_resource} 找到多个资源,请检查资源是否唯一: {res_list}"
else:
return [i[1] for i in res_list]
# 后续加入其他对比方式
self.resource2parent_resource[id(query_resource)] = res_list[0][0]
self.resource2parent_resource[id(res_list[0][1])] = res_list[0][0]
# 后续加入其他对比方式
return res_list[0][1]
def loop_find_resource(self, resource, target_resource_cls_type, identifier_key, compare_value, parent_res=None) -> List[Tuple[Any, Any]]:
@@ -57,8 +61,12 @@ class DeviceNodeResourceTracker(object):
children = getattr(resource, "children", [])
for child in children:
res_list.extend(self.loop_find_resource(child, target_resource_cls_type, identifier_key, compare_value, resource))
if target_resource_cls_type == type(resource) or target_resource_cls_type == dict:
if hasattr(resource, identifier_key):
if target_resource_cls_type == type(resource):
if target_resource_cls_type == dict:
if identifier_key in resource:
if resource[identifier_key] == compare_value:
res_list.append((parent_res, resource))
elif hasattr(resource, identifier_key):
if getattr(resource, identifier_key) == compare_value:
res_list.append((parent_res, resource))
return res_list

View File

@@ -33,7 +33,7 @@ class DeviceClassCreator(Generic[T]):
这个类提供了从任意类创建实例的通用方法。
"""
def __init__(self, cls: Type[T]):
def __init__(self, cls: Type[T], children: Dict[str, Any], resource_tracker: DeviceNodeResourceTracker):
"""
初始化设备类创建器
@@ -42,6 +42,18 @@ class DeviceClassCreator(Generic[T]):
"""
self.device_cls = cls
self.device_instance: Optional[T] = None
self.children = children
self.resource_tracker = resource_tracker
def attach_resource(self):
"""
附加资源到设备类实例
"""
if self.device_instance is not None:
for c in self.children.values():
if c["type"] == "container":
self.resource_tracker.add_resource(c)
def create_instance(self, data: Dict[str, Any]) -> T:
"""
@@ -60,6 +72,7 @@ class DeviceClassCreator(Generic[T]):
}
)
self.post_create()
self.attach_resource()
return self.device_instance
def get_instance(self) -> Optional[T]:
@@ -90,14 +103,15 @@ class PyLabRobotCreator(DeviceClassCreator[T]):
cls: PyLabRobot设备类
children: 子资源字典,用于资源替换
"""
super().__init__(cls)
self.children = children
self.resource_tracker = resource_tracker
super().__init__(cls, children, resource_tracker)
# 检查类是否具有deserialize方法
self.has_deserialize = hasattr(cls, "deserialize") and callable(getattr(cls, "deserialize"))
if not self.has_deserialize:
logger.warning(f"{cls.__name__} 没有deserialize方法将使用标准构造函数")
def attach_resource(self):
pass # 只能增加实例化物料,原来默认物料仅为字典查询
def _process_resource_mapping(self, resource, source_type):
if source_type == dict:
from pylabrobot.resources.resource import Resource
@@ -260,7 +274,7 @@ class ProtocolNodeCreator(DeviceClassCreator[T]):
这个类提供了针对ProtocolNode设备类的实例创建方法处理children参数。
"""
def __init__(self, cls: Type[T], children: Dict[str, Any]):
def __init__(self, cls: Type[T], children: Dict[str, Any], resource_tracker: DeviceNodeResourceTracker):
"""
初始化ProtocolNode设备类创建器
@@ -268,8 +282,7 @@ class ProtocolNodeCreator(DeviceClassCreator[T]):
cls: ProtocolNode设备类
children: 子资源字典,用于资源替换
"""
super().__init__(cls)
self.children = children
super().__init__(cls, children, resource_tracker)
def create_instance(self, data: Dict[str, Any]) -> T:
"""
@@ -282,8 +295,7 @@ class ProtocolNodeCreator(DeviceClassCreator[T]):
ProtocolNode设备类实例
"""
try:
# 创建实例
# 创建实例额外补充一个给protocol node的字段后面考虑取消
data["children"] = self.children
self.device_instance = super(ProtocolNodeCreator, self).create_instance(data)
self.post_create()

View File

@@ -29,23 +29,23 @@ set(action_files
"action/HeatChillStart.action"
"action/HeatChillStop.action"
"action/ProtocolCleanVessel.action"
"action/ProtocolDissolve.action"
"action/ProtocolFilterThrough.action"
"action/ProtocolRunColumn.action"
"action/ProtocolWait.action"
"action/ProtocolWashSolid.action"
"action/ProtocolFilter.action"
"action/ProtocolCentrifuge.action"
"action/ProtocolCrystallize.action"
"action/ProtocolDry.action"
"action/ProtocolPurge.action"
"action/ProtocolStartPurge.action"
"action/ProtocolStartStir.action"
"action/ProtocolStopPurge.action"
"action/ProtocolStopStir.action"
"action/ProtocolTransfer.action"
"action/CleanVessel.action"
"action/Dissolve.action"
"action/FilterThrough.action"
"action/RunColumn.action"
"action/Wait.action"
"action/WashSolid.action"
"action/Filter.action"
"action/Add.action"
"action/Centrifuge.action"
"action/Crystallize.action"
"action/Dry.action"
"action/Purge.action"
"action/StartPurge.action"
"action/StartStir.action"
"action/StopPurge.action"
"action/StopStir.action"
"action/Transfer.action"
"action/LiquidHandlerProtocolCreation.action"
"action/LiquidHandlerAspirate.action"