Compare commits

..

5 Commits

Author SHA1 Message Date
Xuwznln
4139e079f4 Dev (#52)
* 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

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

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

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

* fix move it

* fix move it

* create_resource

* bump ver
modify slot type

* 增加modbus支持
调整protocol node以更好支持多种类型的read和write

* 调整protocol node以更好支持多种类型的read和write

* 补充日志

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

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

* bump version & protocol fix

* hotfix: Add macos_sdk_config (#46)

Co-authored-by: quehh <scienceol@outlook.com>

* include device_mesh when pip install

* 测试自动构建

* try build fix

* try build

* test artifacts

* hotfix: Add .certs in .gitignore

* create container

* container 添加和更新完成

* Device registry port (#49)

* 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

* unify liquid_handler definition

* Update virtual_device.yaml

* 更正了stir和heater的连接方式

* 区分了虚拟仪器中的八通阀和电磁阀,添加了两个阀门的驱动

* 修改了add protocol

* 修复了阀门更新版的bug

* 修复了添加protocol前缀导致的不能启动的bug

* Fix handles

* bump version to 0.9.6

* add resource edge upload

* update container registry and handles

* add virtual_separator virtual_rotavap
fix transfer_pump

* fix container value
add parent_name to edge device id

* 大图的问题都修复好了,添加了gassource和vacuum pump的驱动以及注册表

* default resource upload mode is false

* 添加了icon的文件名在注册表里面

* 修改了json图中link的格式

* fix resource and edge upload

* fix device ports

* Fix edge id

* 移除device的父节点关联

* separate registry sync and resource_add

* 默认不进行注册表报送,通过命令unilabos-register或者增加启动参数

* 完善tip

* protocol node不再嵌套显示

* bump version to 0.9.7  新增一个测试PumpTransferProtocol的teststation,亲测可以运行,将八通阀们和转移泵与pump_protocol适配

* protocol node 执行action不应携带自身device id

* 添加了一套简易双八通阀工作站JSON,亲测能跑

* 修复了很多protocol,亲测能跑

* 添加了run column和filter through的protocol,亲测能跑

* fix mock_reactor

* 修改了大图和小图的json,但是在前端上没看到改变

---------

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: q434343 <73513873+q434343@users.noreply.github.com>
Co-authored-by: Junhan Chang <changjh@pku.edu.cn>

* 更新workstation注册表

* 添加了两个protocol的检索功能 (#51)

* 添加了两个protocol的检索liquid type功能

* fix workstation registry

* 修复了没连接的几个仪器的link,添加了container的icon

* 修改了json和注册表,现在大图全部的device都链接上了

* 修复了小图的json图,线全部连上了

* add work_station protocol handles (ports)

* fix workstation action handle

---------

Co-authored-by: Xuwznln <18435084+Xuwznln@users.noreply.github.com>
Co-authored-by: Junhan Chang <changjh@dp.tech>

---------

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>
Co-authored-by: q434343 <73513873+q434343@users.noreply.github.com>
Co-authored-by: Harvey Que <Q-Query@outlook.com>
Co-authored-by: Kongchang Feng <2100011801@stu.pku.edu.cn>
Co-authored-by: hh. <103566763+Mile-Away@users.noreply.github.com>
Co-authored-by: quehh <scienceol@outlook.com>
Co-authored-by: Harvey Que <quehaohui@dp.tech>
Co-authored-by: Junhan Chang <changjh@dp.tech>
2025-06-22 18:33:08 +08:00
Xuwznln
efc0a9fbbc v0.9.7 (#50)
注册表单独上传、新增大量模拟节点与Protocol、新增container管理、修复pip install出现的文件缺失问题


* 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

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

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

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

* fix move it

* fix move it

* create_resource

* bump ver
modify slot type

* 增加modbus支持
调整protocol node以更好支持多种类型的read和write

* 调整protocol node以更好支持多种类型的read和write

* 补充日志

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

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

* bump version & protocol fix

* hotfix: Add macos_sdk_config (#46)

Co-authored-by: quehh <scienceol@outlook.com>

* include device_mesh when pip install

* 测试自动构建

* try build fix

* try build

* test artifacts

* hotfix: Add .certs in .gitignore

* create container

* container 添加和更新完成

* Device registry port (#49)

* 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

* unify liquid_handler definition

* Update virtual_device.yaml

* 更正了stir和heater的连接方式

* 区分了虚拟仪器中的八通阀和电磁阀,添加了两个阀门的驱动

* 修改了add protocol

* 修复了阀门更新版的bug

* 修复了添加protocol前缀导致的不能启动的bug

* Fix handles

* bump version to 0.9.6

* add resource edge upload

* update container registry and handles

* add virtual_separator virtual_rotavap
fix transfer_pump

* fix container value
add parent_name to edge device id

* 大图的问题都修复好了,添加了gassource和vacuum pump的驱动以及注册表

* default resource upload mode is false

* 添加了icon的文件名在注册表里面

* 修改了json图中link的格式

* fix resource and edge upload

* fix device ports

* Fix edge id

* 移除device的父节点关联

* separate registry sync and resource_add

* 默认不进行注册表报送,通过命令unilabos-register或者增加启动参数

* 完善tip

* protocol node不再嵌套显示

* bump version to 0.9.7  新增一个测试PumpTransferProtocol的teststation,亲测可以运行,将八通阀们和转移泵与pump_protocol适配

* protocol node 执行action不应携带自身device id

* 添加了一套简易双八通阀工作站JSON,亲测能跑

* 修复了很多protocol,亲测能跑

* 添加了run column和filter through的protocol,亲测能跑

* fix mock_reactor

* 修改了大图和小图的json,但是在前端上没看到改变

---------

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: q434343 <73513873+q434343@users.noreply.github.com>
Co-authored-by: Junhan Chang <changjh@pku.edu.cn>

---------

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>
Co-authored-by: q434343 <73513873+q434343@users.noreply.github.com>
Co-authored-by: Harvey Que <Q-Query@outlook.com>
Co-authored-by: Kongchang Feng <2100011801@stu.pku.edu.cn>
Co-authored-by: hh. <103566763+Mile-Away@users.noreply.github.com>
Co-authored-by: quehh <scienceol@outlook.com>
Co-authored-by: Harvey Que <quehaohui@dp.tech>
2025-06-22 13:02:51 +08:00
Xuwznln
7db3123547 0.9.5 Candidate (#47)
* 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

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

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

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

* fix move it

* fix move it

* create_resource

* bump ver
modify slot type

* 增加modbus支持
调整protocol node以更好支持多种类型的read和write

* 调整protocol node以更好支持多种类型的read和write

* 补充日志

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

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

* bump version & protocol fix

* hotfix: Add macos_sdk_config (#46)

Co-authored-by: quehh <scienceol@outlook.com>

* include device_mesh when pip install

---------

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>
Co-authored-by: q434343 <73513873+q434343@users.noreply.github.com>
Co-authored-by: Harvey Que <Q-Query@outlook.com>
Co-authored-by: Kongchang Feng <2100011801@stu.pku.edu.cn>
Co-authored-by: hh. <103566763+Mile-Away@users.noreply.github.com>
Co-authored-by: quehh <scienceol@outlook.com>
2025-06-13 13:35:53 +08:00
Xuwznln
6da7a20a7a bump version & protocol fix (#45)
* 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

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

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

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

* fix move it

* fix move it

* create_resource

* bump ver
modify slot type

* 增加modbus支持
调整protocol node以更好支持多种类型的read和write

* 调整protocol node以更好支持多种类型的read和write

* 补充日志

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

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

* bump version & protocol fix

---------

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>
Co-authored-by: q434343 <73513873+q434343@users.noreply.github.com>
Co-authored-by: Harvey Que <Q-Query@outlook.com>
Co-authored-by: Kongchang Feng <2100011801@stu.pku.edu.cn>
2025-06-12 22:10:30 +08:00
Xuwznln
aa1c67de29 Device Visualization & Mock Devices (#44) [37-biomek-i5i7 (#40), Device visualization (#39), Add Mock Device for Organic Synthesis\添加有机合成的虚拟仪器和Protocol (#43)]
* 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

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

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

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

* fix move it

* fix move it

* create_resource

* bump ver
modify slot type

* 增加modbus支持
调整protocol node以更好支持多种类型的read和write

* 调整protocol node以更好支持多种类型的read和write

* 补充日志

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

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

---------

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>
Co-authored-by: q434343 <73513873+q434343@users.noreply.github.com>
Co-authored-by: Harvey Que <Q-Query@outlook.com>
Co-authored-by: Kongchang Feng <2100011801@stu.pku.edu.cn>
2025-06-12 21:01:04 +08:00
137 changed files with 24426 additions and 473 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 *.graphml
unilabos/device_mesh/view_robot.rviz unilabos/device_mesh/view_robot.rviz
# Certs
**/.certs

View File

@@ -1,3 +1,5 @@
recursive-include unilabos/registry *.yaml recursive-include unilabos/registry *.yaml
recursive-include unilabos/app/web *.html recursive-include unilabos/app/web *.html
recursive-include unilabos/app/web *.css 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 # Currently, you need to install the `unilabos_msgs` package
# You can download the system-specific package from the Release page # 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.7-xxxxx.tar.bz2
# Install PyLabRobot and other prerequisites # Install PyLabRobot and other prerequisites
git clone https://github.com/PyLabRobot/pylabrobot plr_repo 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` 包 # 现阶段,需要安装 `unilabos_msgs` 包
# 可以前往 Release 页面下载系统对应的包进行安装 # 可以前往 Release 页面下载系统对应的包进行安装
conda install ros-humble-unilabos-msgs-0.9.4-xxxxx.tar.bz2 conda install ros-humble-unilabos-msgs-0.9.7-xxxxx.tar.bz2
# 安装PyLabRobot等前置 # 安装PyLabRobot等前置
git clone https://github.com/PyLabRobot/pylabrobot plr_repo 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: package:
name: ros-humble-unilabos-msgs name: ros-humble-unilabos-msgs
version: 0.9.4 version: 0.9.7
source: source:
path: ../../unilabos_msgs path: ../../unilabos_msgs
folder: ros-humble-unilabos-msgs/src/work folder: ros-humble-unilabos-msgs/src/work
@@ -50,12 +50,12 @@ requirements:
- robostack-staging::ros-humble-rosidl-default-generators - robostack-staging::ros-humble-rosidl-default-generators
- robostack-staging::ros-humble-std-msgs - robostack-staging::ros-humble-std-msgs
- robostack-staging::ros-humble-geometry-msgs - robostack-staging::ros-humble-geometry-msgs
- robostack-staging::ros2-distro-mutex=0.6.* - robostack-staging::ros2-distro-mutex=0.5.*
run: run:
- robostack-staging::ros-humble-action-msgs - robostack-staging::ros-humble-action-msgs
- robostack-staging::ros-humble-ros-workspace - robostack-staging::ros-humble-ros-workspace
- robostack-staging::ros-humble-rosidl-default-runtime - robostack-staging::ros-humble-rosidl-default-runtime
- robostack-staging::ros-humble-std-msgs - robostack-staging::ros-humble-std-msgs
- robostack-staging::ros-humble-geometry-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') }} - sel(osx and x86_64): __osx >={{ MACOSX_DEPLOYMENT_TARGET|default('10.14') }}

View File

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

View File

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

View File

@@ -2,4 +2,10 @@
```bash ```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: [ '{}' ] }" 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,563 @@
{
"nodes": [
{
"id": "AddProtocolTestStation",
"name": "添加协议测试站",
"children": [
"transfer_pump_1",
"transfer_pump_2",
"multiway_valve_1",
"multiway_valve_2",
"stirrer_1",
"stirrer_2",
"flask_DMF",
"flask_ethyl_acetate",
"flask_methanol",
"flask_acetone",
"flask_water",
"flask_air",
"main_reactor",
"secondary_reactor",
"waste_workup",
"collection_bottle_1",
"collection_bottle_2"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 500,
"y": 200,
"z": 0
},
"config": {
"protocol_type": ["PumpTransferProtocol", "AddProtocol"]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "转移泵1",
"children": [],
"parent": "AddProtocolTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 250,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_PUMP1",
"max_volume": 25.0,
"transfer_rate": 5.0
},
"data": {
"position": 0.0,
"status": "Idle"
}
},
{
"id": "transfer_pump_2",
"name": "转移泵2",
"children": [],
"parent": "AddProtocolTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 750,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_PUMP2",
"max_volume": 25.0,
"transfer_rate": 5.0
},
"data": {
"position": 0.0,
"status": "Idle"
}
},
{
"id": "multiway_valve_1",
"name": "试剂分配阀",
"children": [],
"parent": "AddProtocolTestStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 250,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_VALVE1",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "multiway_valve_2",
"name": "反应器分配阀",
"children": [],
"parent": "AddProtocolTestStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 750,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_VALVE2",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "stirrer_1",
"name": "主反应器搅拌器",
"children": [],
"parent": "AddProtocolTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 600,
"y": 450,
"z": 0
},
"config": {
"port": "VIRTUAL_STIRRER1",
"max_speed": 1500.0,
"default_speed": 300.0
},
"data": {
"speed": 0.0,
"status": "Stopped"
}
},
{
"id": "stirrer_2",
"name": "副反应器搅拌器",
"children": [],
"parent": "AddProtocolTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 900,
"y": 450,
"z": 0
},
"config": {
"port": "VIRTUAL_STIRRER2",
"max_speed": 1500.0,
"default_speed": 300.0
},
"data": {
"speed": 0.0,
"status": "Stopped"
}
},
{
"id": "flask_DMF",
"name": "DMF试剂瓶",
"children": [],
"parent": "AddProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 50,
"y": 550,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "DMF",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_ethyl_acetate",
"name": "乙酸乙酯试剂瓶",
"children": [],
"parent": "AddProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 150,
"y": 550,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "ethyl_acetate",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_methanol",
"name": "甲醇试剂瓶",
"children": [],
"parent": "AddProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 550,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "methanol",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_acetone",
"name": "丙酮试剂瓶",
"children": [],
"parent": "AddProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 350,
"y": 550,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "acetone",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_water",
"name": "蒸馏水瓶",
"children": [],
"parent": "AddProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 450,
"y": 550,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "water",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "AddProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 550,
"y": 550,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "main_reactor",
"name": "主反应器",
"children": [],
"parent": "AddProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 600,
"y": 500,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "secondary_reactor",
"name": "副反应器",
"children": [],
"parent": "AddProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 900,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "waste_workup",
"name": "废液处理瓶",
"children": [],
"parent": "AddProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 700,
"y": 600,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_bottle_1",
"name": "收集瓶1",
"children": [],
"parent": "AddProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 800,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_bottle_2",
"name": "收集瓶2",
"children": [],
"parent": "AddProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 900,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"id": "link_pump1_valve1",
"source": "transfer_pump_1",
"target": "multiway_valve_1",
"type": "fluid",
"port": {
"transfer_pump_1": "transferpump",
"multiway_valve_1": "transferpump"
}
},
{
"id": "link_pump2_valve2",
"source": "transfer_pump_2",
"target": "multiway_valve_2",
"type": "fluid",
"port": {
"transfer_pump_2": "transferpump",
"multiway_valve_2": "transferpump"
}
},
{
"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_valve1_DMF",
"source": "multiway_valve_1",
"target": "flask_DMF",
"type": "fluid",
"port": {
"multiway_valve_1": "1",
"flask_DMF": "outlet"
}
},
{
"id": "link_valve1_ethyl_acetate",
"source": "multiway_valve_1",
"target": "flask_ethyl_acetate",
"type": "fluid",
"port": {
"multiway_valve_1": "2",
"flask_ethyl_acetate": "outlet"
}
},
{
"id": "link_valve1_methanol",
"source": "multiway_valve_1",
"target": "flask_methanol",
"type": "fluid",
"port": {
"multiway_valve_1": "3",
"flask_methanol": "outlet"
}
},
{
"id": "link_valve1_acetone",
"source": "multiway_valve_1",
"target": "flask_acetone",
"type": "fluid",
"port": {
"multiway_valve_1": "4",
"flask_acetone": "outlet"
}
},
{
"id": "link_valve1_water",
"source": "multiway_valve_1",
"target": "flask_water",
"type": "fluid",
"port": {
"multiway_valve_1": "5",
"flask_water": "outlet"
}
},
{
"id": "link_valve1_air",
"source": "multiway_valve_1",
"target": "flask_air",
"type": "fluid",
"port": {
"multiway_valve_1": "6",
"flask_air": "top"
}
},
{
"id": "link_valve2_main_reactor",
"source": "multiway_valve_2",
"target": "main_reactor",
"type": "fluid",
"port": {
"multiway_valve_2": "2",
"main_reactor": "inlet"
}
},
{
"id": "link_valve2_secondary_reactor",
"source": "multiway_valve_2",
"target": "secondary_reactor",
"type": "fluid",
"port": {
"multiway_valve_2": "3",
"secondary_reactor": "inlet"
}
},
{
"id": "link_valve2_waste",
"source": "multiway_valve_2",
"target": "waste_workup",
"type": "fluid",
"port": {
"multiway_valve_2": "6",
"waste_workup": "inlet"
}
},
{
"id": "link_valve2_collection1",
"source": "multiway_valve_2",
"target": "collection_bottle_1",
"type": "fluid",
"port": {
"multiway_valve_2": "7",
"collection_bottle_1": "inlet"
}
},
{
"id": "link_valve2_collection2",
"source": "multiway_valve_2",
"target": "collection_bottle_2",
"type": "fluid",
"port": {
"multiway_valve_2": "8",
"collection_bottle_2": "inlet"
}
},
{
"id": "link_stirrer1_main_reactor",
"source": "stirrer_1",
"target": "main_reactor",
"type": "mechanical",
"port": {
"stirrer_1": "stirrer_head",
"main_reactor": "stirrer_port"
}
},
{
"id": "link_stirrer2_secondary_reactor",
"source": "stirrer_2",
"target": "secondary_reactor",
"type": "mechanical",
"port": {
"stirrer_2": "stirrer_head",
"secondary_reactor": "stirrer_port"
}
}
]
}

View File

@@ -0,0 +1,438 @@
{
"nodes": [
{
"id": "CentrifugeProtocolTestStation",
"name": "离心协议测试站",
"children": [
"transfer_pump_1",
"transfer_pump_2",
"multiway_valve_1",
"multiway_valve_2",
"centrifuge_1",
"reaction_mixture",
"centrifuge_tube",
"collection_bottle_1",
"flask_water",
"flask_ethanol",
"flask_acetone",
"flask_air",
"waste_workup"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 500,
"y": 200,
"z": 0
},
"config": {
"protocol_type": [
"CentrifugeProtocol",
"PumpTransferProtocol"
]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "主转移泵",
"children": [],
"parent": "CentrifugeProtocolTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 200,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_PUMP1",
"max_volume": 25.0,
"transfer_rate": 2.0
},
"data": {
"position": 0.0,
"status": "Idle"
}
},
{
"id": "transfer_pump_2",
"name": "副转移泵",
"children": [],
"parent": "CentrifugeProtocolTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 400,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_PUMP2",
"max_volume": 25.0,
"transfer_rate": 2.0
},
"data": {
"position": 0.0,
"status": "Idle"
}
},
{
"id": "multiway_valve_1",
"name": "溶剂分配阀",
"children": [],
"parent": "CentrifugeProtocolTestStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 200,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_VALVE1",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "multiway_valve_2",
"name": "样品分配阀",
"children": [],
"parent": "CentrifugeProtocolTestStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 400,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_VALVE2",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "centrifuge_1",
"name": "离心机",
"children": [],
"parent": "CentrifugeProtocolTestStation",
"type": "device",
"class": "virtual_centrifuge",
"position": {
"x": 600,
"y": 350,
"z": 0
},
"config": {
"port": "VIRTUAL_CENTRIFUGE1",
"max_speed": 15000.0,
"max_temp": 40.0,
"min_temp": 4.0
},
"data": {
"status": "Idle"
}
},
{
"id": "reaction_mixture",
"name": "反应混合物",
"children": [],
"parent": "CentrifugeProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 500,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": [
{
"liquid_type": "cell_suspension",
"liquid_volume": 200.0
}
]
}
},
{
"id": "centrifuge_tube",
"name": "离心管",
"children": [],
"parent": "CentrifugeProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 600,
"y": 450,
"z": 0
},
"config": {
"max_volume": 15.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_bottle_1",
"name": "上清液收集瓶",
"children": [],
"parent": "CentrifugeProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 700,
"y": 500,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_water",
"name": "蒸馏水瓶",
"children": [],
"parent": "CentrifugeProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 200,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "water",
"liquid_volume": 900.0
}
]
}
},
{
"id": "flask_ethanol",
"name": "乙醇清洗瓶",
"children": [],
"parent": "CentrifugeProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 300,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "ethanol",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_acetone",
"name": "丙酮清洗瓶",
"children": [],
"parent": "CentrifugeProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "acetone",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "CentrifugeProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "waste_workup",
"name": "废液瓶",
"children": [],
"parent": "CentrifugeProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 800,
"y": 550,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"id": "link_pump1_valve1",
"source": "transfer_pump_1",
"target": "multiway_valve_1",
"type": "fluid",
"port": {
"transfer_pump_1": "transferpump",
"multiway_valve_1": "transferpump"
}
},
{
"id": "link_pump2_valve2",
"source": "transfer_pump_2",
"target": "multiway_valve_2",
"type": "fluid",
"port": {
"transfer_pump_2": "transferpump",
"multiway_valve_2": "transferpump"
}
},
{
"id": "link_valve1_air",
"source": "multiway_valve_1",
"target": "flask_air",
"type": "fluid",
"port": {
"multiway_valve_1": "1",
"flask_air": "top"
}
},
{
"id": "link_valve1_water",
"source": "multiway_valve_1",
"target": "flask_water",
"type": "fluid",
"port": {
"multiway_valve_1": "2",
"flask_water": "outlet"
}
},
{
"id": "link_valve1_ethanol",
"source": "multiway_valve_1",
"target": "flask_ethanol",
"type": "fluid",
"port": {
"multiway_valve_1": "3",
"flask_ethanol": "outlet"
}
},
{
"id": "link_valve1_acetone",
"source": "multiway_valve_1",
"target": "flask_acetone",
"type": "fluid",
"port": {
"multiway_valve_1": "4",
"flask_acetone": "outlet"
}
},
{
"id": "link_valve1_valve2",
"source": "multiway_valve_1",
"target": "multiway_valve_2",
"type": "fluid",
"port": {
"multiway_valve_1": "5",
"multiway_valve_2": "1"
}
},
{
"id": "link_valve2_reaction_mixture",
"source": "multiway_valve_2",
"target": "reaction_mixture",
"type": "fluid",
"port": {
"multiway_valve_2": "2",
"reaction_mixture": "inlet"
}
},
{
"id": "link_valve2_centrifuge_tube",
"source": "multiway_valve_2",
"target": "centrifuge_tube",
"type": "fluid",
"port": {
"multiway_valve_2": "3",
"centrifuge_tube": "inlet"
}
},
{
"id": "link_valve2_collection",
"source": "multiway_valve_2",
"target": "collection_bottle_1",
"type": "fluid",
"port": {
"multiway_valve_2": "4",
"collection_bottle_1": "inlet"
}
},
{
"id": "link_valve2_waste",
"source": "multiway_valve_2",
"target": "waste_workup",
"type": "fluid",
"port": {
"multiway_valve_2": "5",
"waste_workup": "inlet"
}
},
{
"id": "link_centrifuge1_centrifuge_tube",
"source": "centrifuge_1",
"target": "centrifuge_tube",
"type": "transport",
"port": {
"centrifuge_1": "centrifuge",
"centrifuge_tube": "centrifuge_port"
}
}
]
}

View File

@@ -0,0 +1,446 @@
{
"nodes": [
{
"id": "CleanVesselProtocolTestStation",
"name": "容器清洗协议测试站",
"children": [
"transfer_pump_1",
"transfer_pump_2",
"multiway_valve_1",
"multiway_valve_2",
"heatchill_1",
"flask_water",
"flask_acetone",
"flask_ethanol",
"flask_air",
"main_reactor",
"secondary_reactor",
"waste_workup"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 500,
"y": 200,
"z": 0
},
"config": {
"protocol_type": [
"CleanVesselProtocol",
"PumpTransferProtocol",
"HeatChillProtocol",
"HeatChillStartProtocol",
"HeatChillStopProtocol"
]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "主清洗泵",
"children": [],
"parent": "CleanVesselProtocolTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 250,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_PUMP1",
"max_volume": 25.0,
"transfer_rate": 2.5
},
"data": {
"position": 0.0,
"status": "Idle"
}
},
{
"id": "transfer_pump_2",
"name": "副清洗泵",
"children": [],
"parent": "CleanVesselProtocolTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 450,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_PUMP2",
"max_volume": 25.0,
"transfer_rate": 2.5
},
"data": {
"position": 0.0,
"status": "Idle"
}
},
{
"id": "multiway_valve_1",
"name": "溶剂分配阀",
"children": [],
"parent": "CleanVesselProtocolTestStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 250,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_VALVE1",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "multiway_valve_2",
"name": "容器分配阀",
"children": [],
"parent": "CleanVesselProtocolTestStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 450,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_VALVE2",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "heatchill_1",
"name": "加热清洗器",
"children": [],
"parent": "CleanVesselProtocolTestStation",
"type": "device",
"class": "virtual_heatchill",
"position": {
"x": 600,
"y": 350,
"z": 0
},
"config": {
"port": "VIRTUAL_HEATCHILL1",
"max_temp": 100.0,
"min_temp": 10.0,
"max_stir_speed": 500.0
},
"data": {
"status": "Idle"
}
},
{
"id": "flask_water",
"name": "蒸馏水瓶",
"children": [],
"parent": "CleanVesselProtocolTestStation",
"type": "container",
"class": "container",
"position": {
"x": 50,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "water",
"liquid_volume": 900.0
}
]
}
},
{
"id": "flask_acetone",
"name": "丙酮清洗瓶",
"children": [],
"parent": "CleanVesselProtocolTestStation",
"type": "container",
"class": "container",
"position": {
"x": 150,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "acetone",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_ethanol",
"name": "乙醇清洗瓶",
"children": [],
"parent": "CleanVesselProtocolTestStation",
"type": "container",
"class": "container",
"position": {
"x": 250,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "ethanol",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "CleanVesselProtocolTestStation",
"type": "container",
"class": "container",
"position": {
"x": 350,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "main_reactor",
"name": "主反应器",
"children": [],
"parent": "CleanVesselProtocolTestStation",
"type": "container",
"class": "container",
"position": {
"x": 600,
"y": 450,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"liquid_type": "residue",
"liquid_volume": 50.0
}
]
}
},
{
"id": "secondary_reactor",
"name": "副反应器",
"children": [],
"parent": "CleanVesselProtocolTestStation",
"type": "container",
"class": "container",
"position": {
"x": 800,
"y": 450,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "organic_residue",
"liquid_volume": 30.0
}
]
}
},
{
"id": "waste_workup",
"name": "清洗废液瓶",
"children": [],
"parent": "CleanVesselProtocolTestStation",
"type": "container",
"class": "container",
"position": {
"x": 700,
"y": 550,
"z": 0
},
"config": {
"max_volume": 3000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"id": "link_pump1_to_valve1",
"source": "transfer_pump_1",
"target": "multiway_valve_1",
"type": "fluid",
"port": {
"transfer_pump_1": "transferpump",
"multiway_valve_1": "transferpump"
}
},
{
"id": "link_pump2_to_valve2",
"source": "transfer_pump_2",
"target": "multiway_valve_2",
"type": "fluid",
"port": {
"transfer_pump_2": "transferpump",
"multiway_valve_2": "transferpump"
}
},
{
"id": "link_valve1_to_water",
"source": "multiway_valve_1",
"target": "flask_water",
"type": "fluid",
"port": {
"multiway_valve_1": "2",
"flask_water": "top"
}
},
{
"id": "link_valve1_to_acetone",
"source": "multiway_valve_1",
"target": "flask_acetone",
"type": "fluid",
"port": {
"multiway_valve_1": "3",
"flask_acetone": "top"
}
},
{
"id": "link_valve1_to_ethanol",
"source": "multiway_valve_1",
"target": "flask_ethanol",
"type": "fluid",
"port": {
"multiway_valve_1": "4",
"flask_ethanol": "top"
}
},
{
"id": "link_valve1_to_air",
"source": "multiway_valve_1",
"target": "flask_air",
"type": "fluid",
"port": {
"multiway_valve_1": "6",
"flask_air": "top"
}
},
{
"id": "link_valve1_to_valve2_for_cleaning",
"source": "multiway_valve_1",
"target": "multiway_valve_2",
"type": "fluid",
"port": {
"multiway_valve_1": "1",
"multiway_valve_2": "8"
}
},
{
"id": "link_valve2_to_main_reactor_in",
"source": "multiway_valve_2",
"target": "main_reactor",
"type": "fluid",
"port": {
"multiway_valve_2": "2",
"main_reactor": "top"
}
},
{
"id": "link_valve2_to_secondary_reactor_in",
"source": "multiway_valve_2",
"target": "secondary_reactor",
"type": "fluid",
"port": {
"multiway_valve_2": "3",
"secondary_reactor": "top"
}
},
{
"id": "link_main_reactor_out_to_valve2",
"source": "main_reactor",
"target": "multiway_valve_2",
"type": "fluid",
"port": {
"main_reactor": "bottom",
"multiway_valve_2": "6"
}
},
{
"id": "link_secondary_reactor_out_to_valve2",
"source": "secondary_reactor",
"target": "multiway_valve_2",
"type": "fluid",
"port": {
"secondary_reactor": "bottom",
"multiway_valve_2": "7"
}
},
{
"id": "link_valve2_to_waste",
"source": "multiway_valve_2",
"target": "waste_workup",
"type": "fluid",
"port": {
"multiway_valve_2": "4",
"waste_workup": "top"
}
},
{
"id": "link_heatchill1_to_main_reactor",
"source": "heatchill_1",
"target": "main_reactor",
"type": "mechanical",
"port": {
"heatchill_1": "heatchill",
"main_reactor": "bind"
}
},
{
"id": "link_heatchill1_to_secondary_reactor",
"source": "heatchill_1",
"target": "secondary_reactor",
"type": "mechanical",
"port": {
"heatchill_1": "heatchill",
"secondary_reactor": "bind"
}
}
]
}

View File

@@ -0,0 +1,367 @@
{
"nodes": [
{
"id": "DualValvePumpStation",
"name": "双阀门泵站",
"children": [
"transfer_pump_1",
"transfer_pump_2",
"multiway_valve_1",
"multiway_valve_2",
"flask_DMF",
"flask_ethyl_acetate",
"flask_methanol",
"flask_air",
"main_reactor",
"waste_workup",
"collection_bottle_1"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 500,
"y": 200,
"z": 0
},
"config": {
"protocol_type": ["PumpTransferProtocol"]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "转移泵1",
"children": [],
"parent": "DualValvePumpStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 300,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_PUMP1",
"max_volume": 25.0,
"transfer_rate": 5.0
},
"data": {
"position": 0.0,
"status": "Idle"
}
},
{
"id": "transfer_pump_2",
"name": "转移泵2",
"children": [],
"parent": "DualValvePumpStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 700,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_PUMP2",
"max_volume": 25.0,
"transfer_rate": 5.0
},
"data": {
"position": 0.0,
"status": "Idle"
}
},
{
"id": "multiway_valve_1",
"name": "第一个八通阀",
"children": [],
"parent": "DualValvePumpStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 300,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_VALVE1",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "multiway_valve_2",
"name": "第二个八通阀",
"children": [],
"parent": "DualValvePumpStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 700,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_VALVE2",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "flask_DMF",
"name": "DMF试剂瓶",
"children": [],
"parent": "DualValvePumpStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "DMF",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_ethyl_acetate",
"name": "乙酸乙酯试剂瓶",
"children": [],
"parent": "DualValvePumpStation",
"type": "container",
"class": null,
"position": {
"x": 200,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "ethyl_acetate",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_methanol",
"name": "甲醇试剂瓶",
"children": [],
"parent": "DualValvePumpStation",
"type": "container",
"class": null,
"position": {
"x": 300,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "methanol",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "DualValvePumpStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "main_reactor",
"name": "主反应器",
"children": [],
"parent": "DualValvePumpStation",
"type": "container",
"class": null,
"position": {
"x": 600,
"y": 500,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "waste_workup",
"name": "废液处理瓶",
"children": [],
"parent": "DualValvePumpStation",
"type": "container",
"class": null,
"position": {
"x": 700,
"y": 500,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_bottle_1",
"name": "收集瓶1",
"children": [],
"parent": "DualValvePumpStation",
"type": "container",
"class": null,
"position": {
"x": 800,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"id": "link_pump1_valve1",
"source": "transfer_pump_1",
"target": "multiway_valve_1",
"type": "fluid",
"port": {
"transfer_pump_1": "transferpump",
"multiway_valve_1": "transferpump"
}
},
{
"id": "link_pump2_valve2",
"source": "transfer_pump_2",
"target": "multiway_valve_2",
"type": "fluid",
"port": {
"transfer_pump_2": "transferpump",
"multiway_valve_2": "transferpump"
}
},
{
"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_valve1_air",
"source": "multiway_valve_1",
"target": "flask_air",
"type": "fluid",
"port": {
"multiway_valve_1": "1",
"flask_air": "top"
}
},
{
"id": "link_valve1_DMF",
"source": "multiway_valve_1",
"target": "flask_DMF",
"type": "fluid",
"port": {
"multiway_valve_1": "2",
"flask_DMF": "outlet"
}
},
{
"id": "link_valve1_ethyl_acetate",
"source": "multiway_valve_1",
"target": "flask_ethyl_acetate",
"type": "fluid",
"port": {
"multiway_valve_1": "3",
"flask_ethyl_acetate": "outlet"
}
},
{
"id": "link_valve1_methanol",
"source": "multiway_valve_1",
"target": "flask_methanol",
"type": "fluid",
"port": {
"multiway_valve_1": "4",
"flask_methanol": "outlet"
}
},
{
"id": "link_valve2_reactor",
"source": "multiway_valve_2",
"target": "main_reactor",
"type": "fluid",
"port": {
"multiway_valve_2": "5",
"main_reactor": "inlet"
}
},
{
"id": "link_valve2_waste",
"source": "multiway_valve_2",
"target": "waste_workup",
"type": "fluid",
"port": {
"multiway_valve_2": "6",
"waste_workup": "inlet"
}
},
{
"id": "link_valve2_collection",
"source": "multiway_valve_2",
"target": "collection_bottle_1",
"type": "fluid",
"port": {
"multiway_valve_2": "7",
"collection_bottle_1": "inlet"
}
}
]
}

View File

@@ -0,0 +1,557 @@
{
"nodes": [
{
"id": "EvacuateRefillTestStation",
"name": "抽真空充气测试站",
"children": [
"transfer_pump_1",
"transfer_pump_2",
"multiway_valve_1",
"multiway_valve_2",
"flask_DMF",
"flask_ethyl_acetate",
"flask_methanol",
"flask_air",
"vacuum_pump_1",
"gas_source_nitrogen",
"gas_source_air",
"solenoid_valve_vacuum",
"solenoid_valve_gas",
"main_reactor",
"stirrer_1",
"waste_workup",
"collection_bottle_1"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 500,
"y": 200,
"z": 0
},
"config": {
"protocol_type": ["PumpTransferProtocol", "EvacuateAndRefillProtocol"]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "转移泵1",
"children": [],
"parent": "EvacuateRefillTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 300,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_PUMP1",
"max_volume": 25.0,
"transfer_rate": 5.0
},
"data": {
"position": 0.0,
"status": "Idle"
}
},
{
"id": "transfer_pump_2",
"name": "转移泵2",
"children": [],
"parent": "EvacuateRefillTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 700,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_PUMP2",
"max_volume": 25.0,
"transfer_rate": 5.0
},
"data": {
"position": 0.0,
"status": "Idle"
}
},
{
"id": "multiway_valve_1",
"name": "第一个八通阀",
"children": [],
"parent": "EvacuateRefillTestStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 300,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_VALVE1",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "multiway_valve_2",
"name": "第二个八通阀",
"children": [],
"parent": "EvacuateRefillTestStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 700,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_VALVE2",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "vacuum_pump_1",
"name": "真空泵1",
"children": [],
"parent": "EvacuateRefillTestStation",
"type": "device",
"class": "virtual_vacuum_pump",
"position": {
"x": 150,
"y": 200,
"z": 0
},
"config": {
"port": "VIRTUAL_VACUUM1",
"max_pressure": -0.9
},
"data": {
"status": "OFF",
"pressure": 0.0
}
},
{
"id": "gas_source_nitrogen",
"name": "氮气源",
"children": [],
"parent": "EvacuateRefillTestStation",
"type": "device",
"class": "virtual_gas_source",
"position": {
"x": 850,
"y": 200,
"z": 0
},
"config": {
"port": "VIRTUAL_GAS_N2",
"gas_type": "nitrogen",
"max_pressure": 5.0
},
"data": {
"status": "OFF",
"flow_rate": 0.0
}
},
{
"id": "gas_source_air",
"name": "空气源",
"children": [],
"parent": "EvacuateRefillTestStation",
"type": "device",
"class": "virtual_gas_source",
"position": {
"x": 950,
"y": 200,
"z": 0
},
"config": {
"port": "VIRTUAL_GAS_AIR",
"gas_type": "air",
"max_pressure": 3.0
},
"data": {
"status": "OFF",
"flow_rate": 0.0
}
},
{
"id": "solenoid_valve_vacuum",
"name": "真空电磁阀",
"children": [],
"parent": "EvacuateRefillTestStation",
"type": "device",
"class": "virtual_solenoid_valve",
"position": {
"x": 225,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_SOLENOID_VACUUM"
},
"data": {
"valve_position": "CLOSED"
}
},
{
"id": "solenoid_valve_gas",
"name": "气源电磁阀",
"children": [],
"parent": "EvacuateRefillTestStation",
"type": "device",
"class": "virtual_solenoid_valve",
"position": {
"x": 775,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_SOLENOID_GAS"
},
"data": {
"valve_position": "CLOSED"
}
},
{
"id": "flask_DMF",
"name": "DMF试剂瓶",
"children": [],
"parent": "EvacuateRefillTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "DMF",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_ethyl_acetate",
"name": "乙酸乙酯试剂瓶",
"children": [],
"parent": "EvacuateRefillTestStation",
"type": "container",
"class": null,
"position": {
"x": 200,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "ethyl_acetate",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_methanol",
"name": "甲醇试剂瓶",
"children": [],
"parent": "EvacuateRefillTestStation",
"type": "container",
"class": null,
"position": {
"x": 300,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "methanol",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "EvacuateRefillTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "main_reactor",
"name": "主反应器",
"children": [],
"parent": "EvacuateRefillTestStation",
"type": "container",
"class": null,
"position": {
"x": 600,
"y": 500,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "stirrer_1",
"name": "搅拌器1",
"children": [],
"parent": "EvacuateRefillTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 600,
"y": 450,
"z": 0
},
"config": {
"port": "VIRTUAL_STIRRER1",
"max_speed": 1500.0
},
"data": {
"speed": 0.0,
"status": "OFF"
}
},
{
"id": "waste_workup",
"name": "废液处理瓶",
"children": [],
"parent": "EvacuateRefillTestStation",
"type": "container",
"class": null,
"position": {
"x": 700,
"y": 500,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_bottle_1",
"name": "收集瓶1",
"children": [],
"parent": "EvacuateRefillTestStation",
"type": "container",
"class": null,
"position": {
"x": 800,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"id": "link_pump1_valve1",
"source": "transfer_pump_1",
"target": "multiway_valve_1",
"type": "fluid",
"port": {
"transfer_pump_1": "transferpump",
"multiway_valve_1": "transferpump"
}
},
{
"id": "link_pump2_valve2",
"source": "transfer_pump_2",
"target": "multiway_valve_2",
"type": "fluid",
"port": {
"transfer_pump_2": "transferpump",
"multiway_valve_2": "transferpump"
}
},
{
"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_vacuum_solenoid",
"source": "vacuum_pump_1",
"target": "solenoid_valve_vacuum",
"type": "fluid",
"port": {
"vacuum_pump_1": "outlet",
"solenoid_valve_vacuum": "inlet"
}
},
{
"id": "link_solenoid_vacuum_valve1",
"source": "solenoid_valve_vacuum",
"target": "multiway_valve_1",
"type": "fluid",
"port": {
"solenoid_valve_vacuum": "outlet",
"multiway_valve_1": "7"
}
},
{
"id": "link_gas_solenoid",
"source": "gas_source_nitrogen",
"target": "solenoid_valve_gas",
"type": "fluid",
"port": {
"gas_source_nitrogen": "outlet",
"solenoid_valve_gas": "inlet"
}
},
{
"id": "link_solenoid_gas_valve2",
"source": "solenoid_valve_gas",
"target": "multiway_valve_2",
"type": "fluid",
"port": {
"solenoid_valve_gas": "outlet",
"multiway_valve_2": "8"
}
},
{
"id": "link_air_source_valve2",
"source": "gas_source_air",
"target": "multiway_valve_2",
"type": "fluid",
"port": {
"gas_source_air": "outlet",
"multiway_valve_2": "2"
}
},
{
"id": "link_valve1_air",
"source": "multiway_valve_1",
"target": "flask_air",
"type": "fluid",
"port": {
"multiway_valve_1": "1",
"flask_air": "top"
}
},
{
"id": "link_valve1_DMF",
"source": "multiway_valve_1",
"target": "flask_DMF",
"type": "fluid",
"port": {
"multiway_valve_1": "2",
"flask_DMF": "outlet"
}
},
{
"id": "link_valve1_ethyl_acetate",
"source": "multiway_valve_1",
"target": "flask_ethyl_acetate",
"type": "fluid",
"port": {
"multiway_valve_1": "3",
"flask_ethyl_acetate": "outlet"
}
},
{
"id": "link_valve1_methanol",
"source": "multiway_valve_1",
"target": "flask_methanol",
"type": "fluid",
"port": {
"multiway_valve_1": "4",
"flask_methanol": "outlet"
}
},
{
"id": "link_valve2_reactor",
"source": "multiway_valve_2",
"target": "main_reactor",
"type": "fluid",
"port": {
"multiway_valve_2": "5",
"main_reactor": "inlet"
}
},
{
"id": "link_valve2_waste",
"source": "multiway_valve_2",
"target": "waste_workup",
"type": "fluid",
"port": {
"multiway_valve_2": "6",
"waste_workup": "inlet"
}
},
{
"id": "link_valve2_collection",
"source": "multiway_valve_2",
"target": "collection_bottle_1",
"type": "fluid",
"port": {
"multiway_valve_2": "7",
"collection_bottle_1": "inlet"
}
},
{
"id": "link_stirrer_reactor",
"source": "stirrer_1",
"target": "main_reactor",
"type": "mechanical",
"port": {
"stirrer_1": "stirrer",
"main_reactor": "stirrer"
}
}
]
}

View File

@@ -0,0 +1,503 @@
{
"nodes": [
{
"id": "EvaporateProtocolTestStation",
"name": "蒸发协议测试站",
"children": [
"transfer_pump_1",
"transfer_pump_2",
"multiway_valve_1",
"multiway_valve_2",
"rotavap_1",
"heatchill_1",
"reaction_mixture",
"rotavap_flask",
"rotavap_condenser",
"flask_distillate",
"flask_ethanol",
"flask_acetone",
"flask_water",
"flask_air",
"waste_workup"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 500,
"y": 200,
"z": 0
},
"config": {
"protocol_type": [
"EvaporateProtocol",
"PumpTransferProtocol",
"HeatChillProtocol",
"HeatChillStartProtocol",
"HeatChillStopProtocol"
]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "主转移泵",
"children": [],
"parent": "EvaporateProtocolTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 200,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_PUMP1",
"max_volume": 25.0,
"transfer_rate": 2.5
},
"data": {
"position": 0.0,
"status": "Idle"
}
},
{
"id": "transfer_pump_2",
"name": "副转移泵",
"children": [],
"parent": "EvaporateProtocolTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 400,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_PUMP2",
"max_volume": 25.0,
"transfer_rate": 2.5
},
"data": {
"position": 0.0,
"status": "Idle"
}
},
{
"id": "multiway_valve_1",
"name": "溶剂分配阀",
"children": [],
"parent": "EvaporateProtocolTestStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 200,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_VALVE1",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "multiway_valve_2",
"name": "容器分配阀",
"children": [],
"parent": "EvaporateProtocolTestStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 400,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_VALVE2",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "rotavap_1",
"name": "旋转蒸发仪",
"children": [],
"parent": "EvaporateProtocolTestStation",
"type": "device",
"class": "virtual_rotavap",
"position": {
"x": 700,
"y": 350,
"z": 0
},
"config": {
"port": "VIRTUAL_ROTAVAP1",
"max_temp": 180.0,
"max_rotation_speed": 280.0
},
"data": {
"status": "Ready"
}
},
{
"id": "heatchill_1",
"name": "预加热器",
"children": [],
"parent": "EvaporateProtocolTestStation",
"type": "device",
"class": "virtual_heatchill",
"position": {
"x": 100,
"y": 550,
"z": 0
},
"config": {
"port": "VIRTUAL_HEATCHILL1",
"max_temp": 100.0,
"min_temp": 10.0,
"max_stir_speed": 500.0
},
"data": {
"status": "Idle"
}
},
{
"id": "reaction_mixture",
"name": "反应混合物",
"children": [],
"parent": "EvaporateProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 450,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "reaction_mixture",
"liquid_volume": 600.0
}
]
}
},
{
"id": "rotavap_flask",
"name": "旋蒸样品瓶",
"children": [],
"parent": "EvaporateProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 700,
"y": 450,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": []
}
},
{
"id": "rotavap_condenser",
"name": "旋蒸冷凝器",
"children": [],
"parent": "EvaporateProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 800,
"y": 350,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_distillate",
"name": "溶剂回收瓶",
"children": [],
"parent": "EvaporateProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 800,
"y": 450,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_ethanol",
"name": "乙醇清洗瓶",
"children": [],
"parent": "EvaporateProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 50,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "ethanol",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_acetone",
"name": "丙酮清洗瓶",
"children": [],
"parent": "EvaporateProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 150,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "acetone",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_water",
"name": "蒸馏水瓶",
"children": [],
"parent": "EvaporateProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "water",
"liquid_volume": 900.0
}
]
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "EvaporateProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 350,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "waste_workup",
"name": "废液瓶",
"children": [],
"parent": "EvaporateProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 600,
"y": 550,
"z": 0
},
"config": {
"max_volume": 3000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"id": "link_pump1_valve1",
"source": "transfer_pump_1",
"target": "multiway_valve_1",
"type": "fluid",
"port": {
"transfer_pump_1": "transferpump",
"multiway_valve_1": "transferpump"
}
},
{
"id": "link_pump2_valve2",
"source": "transfer_pump_2",
"target": "multiway_valve_2",
"type": "fluid",
"port": {
"transfer_pump_2": "transferpump",
"multiway_valve_2": "transferpump"
}
},
{
"id": "link_valve1_air",
"source": "multiway_valve_1",
"target": "flask_air",
"type": "fluid",
"port": {
"multiway_valve_1": "1",
"flask_air": "top"
}
},
{
"id": "link_valve1_ethanol",
"source": "multiway_valve_1",
"target": "flask_ethanol",
"type": "fluid",
"port": {
"multiway_valve_1": "2",
"flask_ethanol": "outlet"
}
},
{
"id": "link_valve1_acetone",
"source": "multiway_valve_1",
"target": "flask_acetone",
"type": "fluid",
"port": {
"multiway_valve_1": "3",
"flask_acetone": "outlet"
}
},
{
"id": "link_valve1_water",
"source": "multiway_valve_1",
"target": "flask_water",
"type": "fluid",
"port": {
"multiway_valve_1": "4",
"flask_water": "outlet"
}
},
{
"id": "link_valve1_valve2",
"source": "multiway_valve_1",
"target": "multiway_valve_2",
"type": "fluid",
"port": {
"multiway_valve_1": "5",
"multiway_valve_2": "1"
}
},
{
"id": "link_valve2_reaction_mixture",
"source": "multiway_valve_2",
"target": "reaction_mixture",
"type": "fluid",
"port": {
"multiway_valve_2": "2",
"reaction_mixture": "inlet"
}
},
{
"id": "link_valve2_rotavap_flask",
"source": "multiway_valve_2",
"target": "rotavap_flask",
"type": "fluid",
"port": {
"multiway_valve_2": "3",
"rotavap_flask": "inlet"
}
},
{
"id": "link_valve2_rotavap_condenser",
"source": "multiway_valve_2",
"target": "rotavap_condenser",
"type": "fluid",
"port": {
"multiway_valve_2": "4",
"rotavap_condenser": "inlet"
}
},
{
"id": "link_valve2_distillate",
"source": "multiway_valve_2",
"target": "flask_distillate",
"type": "fluid",
"port": {
"multiway_valve_2": "5",
"flask_distillate": "inlet"
}
},
{
"id": "link_valve2_waste",
"source": "multiway_valve_2",
"target": "waste_workup",
"type": "fluid",
"port": {
"multiway_valve_2": "6",
"waste_workup": "inlet"
}
},
{
"id": "link_rotavap1_rotavap_flask",
"source": "rotavap_1",
"target": "rotavap_flask",
"type": "fluid",
"port": {
"rotavap_1": "rotavap-sample",
"rotavap_flask": "rotavap_port"
}
},
{
"id": "link_heatchill1_reaction_mixture",
"source": "heatchill_1",
"target": "reaction_mixture",
"type": "mechanical",
"port": {
"heatchill_1": "heatchill",
"reaction_mixture": "heating_jacket"
}
}
]
}

View File

@@ -0,0 +1,534 @@
{
"nodes": [
{
"id": "FilterProtocolTestStation",
"name": "过滤协议测试站",
"children": [
"transfer_pump_1",
"transfer_pump_2",
"multiway_valve_1",
"multiway_valve_2",
"filter_1",
"heatchill_1",
"reaction_mixture",
"filter_vessel",
"filtrate_vessel",
"collection_bottle_1",
"collection_bottle_2",
"flask_water",
"flask_ethanol",
"flask_acetone",
"flask_air",
"waste_workup"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 500,
"y": 200,
"z": 0
},
"config": {
"protocol_type": [
"FilterProtocol",
"PumpTransferProtocol",
"HeatChillProtocol",
"HeatChillStartProtocol",
"HeatChillStopProtocol"
]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "主转移泵",
"children": [],
"parent": "FilterProtocolTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 200,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_PUMP1",
"max_volume": 25.0,
"transfer_rate": 2.0
},
"data": {
"position": 0.0,
"status": "Idle"
}
},
{
"id": "transfer_pump_2",
"name": "副转移泵",
"children": [],
"parent": "FilterProtocolTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 400,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_PUMP2",
"max_volume": 25.0,
"transfer_rate": 2.0
},
"data": {
"position": 0.0,
"status": "Idle"
}
},
{
"id": "multiway_valve_1",
"name": "溶剂分配阀",
"children": [],
"parent": "FilterProtocolTestStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 200,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_VALVE1",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "multiway_valve_2",
"name": "样品分配阀",
"children": [],
"parent": "FilterProtocolTestStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 400,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_VALVE2",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "filter_1",
"name": "过滤器",
"children": [],
"parent": "FilterProtocolTestStation",
"type": "device",
"class": "virtual_filter",
"position": {
"x": 600,
"y": 350,
"z": 0
},
"config": {
"port": "VIRTUAL_FILTER1",
"max_temp": 100.0,
"max_stir_speed": 1000.0,
"max_volume": 500.0
},
"data": {
"status": "Idle"
}
},
{
"id": "heatchill_1",
"name": "加热搅拌器",
"children": [],
"parent": "FilterProtocolTestStation",
"type": "device",
"class": "virtual_heatchill",
"position": {
"x": 600,
"y": 450,
"z": 0
},
"config": {
"port": "VIRTUAL_HEATCHILL1",
"max_temp": 100.0,
"min_temp": 4.0,
"max_stir_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "reaction_mixture",
"name": "反应混合物",
"children": [],
"parent": "FilterProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "cell_suspension",
"liquid_volume": 200.0
}
]
}
},
{
"id": "filter_vessel",
"name": "过滤器容器",
"children": [],
"parent": "FilterProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 600,
"y": 550,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": []
}
},
{
"id": "filtrate_vessel",
"name": "滤液收集容器",
"children": [],
"parent": "FilterProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 700,
"y": 500,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_bottle_1",
"name": "收集瓶1",
"children": [],
"parent": "FilterProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 800,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_bottle_2",
"name": "收集瓶2",
"children": [],
"parent": "FilterProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 900,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_water",
"name": "蒸馏水瓶",
"children": [],
"parent": "FilterProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 200,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "water",
"liquid_volume": 900.0
}
]
}
},
{
"id": "flask_ethanol",
"name": "乙醇清洗瓶",
"children": [],
"parent": "FilterProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 300,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "ethanol",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_acetone",
"name": "丙酮清洗瓶",
"children": [],
"parent": "FilterProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "acetone",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "FilterProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "waste_workup",
"name": "废液瓶",
"children": [],
"parent": "FilterProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 800,
"y": 600,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"id": "link_pump1_valve1",
"source": "transfer_pump_1",
"target": "multiway_valve_1",
"type": "fluid",
"port": {
"transfer_pump_1": "transferpump",
"multiway_valve_1": "transferpump"
}
},
{
"id": "link_pump2_valve2",
"source": "transfer_pump_2",
"target": "multiway_valve_2",
"type": "fluid",
"port": {
"transfer_pump_2": "transferpump",
"multiway_valve_2": "transferpump"
}
},
{
"id": "link_valve1_air",
"source": "multiway_valve_1",
"target": "flask_air",
"type": "fluid",
"port": {
"multiway_valve_1": "1",
"flask_air": "top"
}
},
{
"id": "link_valve1_water",
"source": "multiway_valve_1",
"target": "flask_water",
"type": "fluid",
"port": {
"multiway_valve_1": "2",
"flask_water": "outlet"
}
},
{
"id": "link_valve1_ethanol",
"source": "multiway_valve_1",
"target": "flask_ethanol",
"type": "fluid",
"port": {
"multiway_valve_1": "3",
"flask_ethanol": "outlet"
}
},
{
"id": "link_valve1_acetone",
"source": "multiway_valve_1",
"target": "flask_acetone",
"type": "fluid",
"port": {
"multiway_valve_1": "4",
"flask_acetone": "outlet"
}
},
{
"id": "link_valve1_valve2",
"source": "multiway_valve_1",
"target": "multiway_valve_2",
"type": "fluid",
"port": {
"multiway_valve_1": "5",
"multiway_valve_2": "1"
}
},
{
"id": "link_valve2_reaction_mixture",
"source": "multiway_valve_2",
"target": "reaction_mixture",
"type": "fluid",
"port": {
"multiway_valve_2": "2",
"reaction_mixture": "inlet"
}
},
{
"id": "link_valve2_filter_vessel",
"source": "multiway_valve_2",
"target": "filter_vessel",
"type": "fluid",
"port": {
"multiway_valve_2": "3",
"filter_vessel": "inlet"
}
},
{
"id": "link_valve2_filtrate_vessel",
"source": "multiway_valve_2",
"target": "filtrate_vessel",
"type": "fluid",
"port": {
"multiway_valve_2": "4",
"filtrate_vessel": "inlet"
}
},
{
"id": "link_valve2_collection1",
"source": "multiway_valve_2",
"target": "collection_bottle_1",
"type": "fluid",
"port": {
"multiway_valve_2": "5",
"collection_bottle_1": "inlet"
}
},
{
"id": "link_valve2_collection2",
"source": "multiway_valve_2",
"target": "collection_bottle_2",
"type": "fluid",
"port": {
"multiway_valve_2": "6",
"collection_bottle_2": "inlet"
}
},
{
"id": "link_valve2_waste",
"source": "multiway_valve_2",
"target": "waste_workup",
"type": "fluid",
"port": {
"multiway_valve_2": "7",
"waste_workup": "inlet"
}
},
{
"id": "link_filter1_filter_vessel",
"source": "filter_1",
"target": "filter_vessel",
"type": "transport",
"port": {
"filter_1": "filter",
"filter_vessel": "filter_port"
}
},
{
"id": "link_heatchill1_filter_vessel",
"source": "heatchill_1",
"target": "filter_vessel",
"type": "mechanical",
"port": {
"heatchill_1": "heatchill",
"filter_vessel": "heating_jacket"
}
}
]
}

View File

@@ -0,0 +1,671 @@
{
"nodes": [
{
"id": "HeatChillProtocolTestStation",
"name": "加热冷却协议测试站",
"children": [
"transfer_pump_1",
"transfer_pump_2",
"multiway_valve_1",
"multiway_valve_2",
"stirrer_1",
"stirrer_2",
"heatchill_1",
"heatchill_2",
"flask_DMF",
"flask_ethyl_acetate",
"flask_methanol",
"flask_acetone",
"flask_water",
"flask_ethanol",
"flask_air",
"main_reactor",
"secondary_reactor",
"waste_workup",
"collection_bottle_1",
"collection_bottle_2"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 500,
"y": 200,
"z": 0
},
"config": {
"protocol_type": [
"PumpTransferProtocol",
"AddProtocol",
"HeatChillProtocol",
"HeatChillStartProtocol",
"HeatChillStopProtocol",
"DissolveProtocol"
]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "转移泵1",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 250,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_PUMP1",
"max_volume": 25.0,
"transfer_rate": 5.0
},
"data": {
"position": 0.0,
"status": "Idle"
}
},
{
"id": "transfer_pump_2",
"name": "转移泵2",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 750,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_PUMP2",
"max_volume": 25.0,
"transfer_rate": 5.0
},
"data": {
"position": 0.0,
"status": "Idle"
}
},
{
"id": "multiway_valve_1",
"name": "试剂分配阀",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 250,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_VALVE1",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "multiway_valve_2",
"name": "反应器分配阀",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 750,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_VALVE2",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "stirrer_1",
"name": "主反应器搅拌器",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 600,
"y": 450,
"z": 0
},
"config": {
"port": "VIRTUAL_STIRRER1",
"max_speed": 1500.0,
"default_speed": 300.0
},
"data": {
"speed": 0.0,
"status": "Stopped"
}
},
{
"id": "stirrer_2",
"name": "副反应器搅拌器",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 900,
"y": 450,
"z": 0
},
"config": {
"port": "VIRTUAL_STIRRER2",
"max_speed": 1500.0,
"default_speed": 300.0
},
"data": {
"speed": 0.0,
"status": "Stopped"
}
},
{
"id": "heatchill_1",
"name": "主反应器加热冷却器",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "device",
"class": "virtual_heatchill",
"position": {
"x": 550,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_HEATCHILL1",
"max_temp": 200.0,
"min_temp": -80.0,
"max_stir_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "heatchill_2",
"name": "副反应器加热冷却器",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "device",
"class": "virtual_heatchill",
"position": {
"x": 850,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_HEATCHILL2",
"max_temp": 200.0,
"min_temp": -80.0,
"max_stir_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "flask_DMF",
"name": "DMF试剂瓶",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 50,
"y": 550,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "DMF",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_ethyl_acetate",
"name": "乙酸乙酯试剂瓶",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 150,
"y": 550,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "ethyl_acetate",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_methanol",
"name": "甲醇试剂瓶",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 550,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "methanol",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_ethanol",
"name": "乙醇试剂瓶",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 650,
"y": 550,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "ethanol",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_acetone",
"name": "丙酮试剂瓶",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 350,
"y": 550,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "acetone",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_water",
"name": "蒸馏水瓶",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 450,
"y": 550,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "water",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 550,
"y": 550,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "main_reactor",
"name": "主反应器",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 600,
"y": 500,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "secondary_reactor",
"name": "副反应器",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 900,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "waste_workup",
"name": "废液处理瓶",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 700,
"y": 600,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_bottle_1",
"name": "收集瓶1",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 800,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_bottle_2",
"name": "收集瓶2",
"children": [],
"parent": "HeatChillProtocolTestStation",
"type": "container",
"class": null,
"position": {
"x": 900,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"id": "link_pump1_valve1",
"source": "transfer_pump_1",
"target": "multiway_valve_1",
"type": "fluid",
"port": {
"transfer_pump_1": "transferpump",
"multiway_valve_1": "transferpump"
}
},
{
"id": "link_pump2_valve2",
"source": "transfer_pump_2",
"target": "multiway_valve_2",
"type": "fluid",
"port": {
"transfer_pump_2": "transferpump",
"multiway_valve_2": "transferpump"
}
},
{
"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_valve1_DMF",
"source": "multiway_valve_1",
"target": "flask_DMF",
"type": "fluid",
"port": {
"multiway_valve_1": "1",
"flask_DMF": "outlet"
}
},
{
"id": "link_valve1_ethyl_acetate",
"source": "multiway_valve_1",
"target": "flask_ethyl_acetate",
"type": "fluid",
"port": {
"multiway_valve_1": "2",
"flask_ethyl_acetate": "outlet"
}
},
{
"id": "link_valve1_methanol",
"source": "multiway_valve_1",
"target": "flask_methanol",
"type": "fluid",
"port": {
"multiway_valve_1": "3",
"flask_methanol": "outlet"
}
},
{
"id": "link_valve1_acetone",
"source": "multiway_valve_1",
"target": "flask_acetone",
"type": "fluid",
"port": {
"multiway_valve_1": "4",
"flask_acetone": "outlet"
}
},
{
"id": "link_valve1_water",
"source": "multiway_valve_1",
"target": "flask_water",
"type": "fluid",
"port": {
"multiway_valve_1": "5",
"flask_water": "outlet"
}
},
{
"id": "link_valve1_air",
"source": "multiway_valve_1",
"target": "flask_air",
"type": "fluid",
"port": {
"multiway_valve_1": "6",
"flask_air": "top"
}
},
{
"id": "link_valve2_main_reactor",
"source": "multiway_valve_2",
"target": "main_reactor",
"type": "fluid",
"port": {
"multiway_valve_2": "2",
"main_reactor": "inlet"
}
},
{
"id": "link_valve2_secondary_reactor",
"source": "multiway_valve_2",
"target": "secondary_reactor",
"type": "fluid",
"port": {
"multiway_valve_2": "3",
"secondary_reactor": "inlet"
}
},
{
"id": "link_valve2_waste",
"source": "multiway_valve_2",
"target": "waste_workup",
"type": "fluid",
"port": {
"multiway_valve_2": "6",
"waste_workup": "inlet"
}
},
{
"id": "link_valve2_collection1",
"source": "multiway_valve_2",
"target": "collection_bottle_1",
"type": "fluid",
"port": {
"multiway_valve_2": "7",
"collection_bottle_1": "inlet"
}
},
{
"id": "link_valve2_collection2",
"source": "multiway_valve_2",
"target": "collection_bottle_2",
"type": "fluid",
"port": {
"multiway_valve_2": "8",
"collection_bottle_2": "inlet"
}
},
{
"id": "link_stirrer1_main_reactor",
"source": "stirrer_1",
"target": "main_reactor",
"type": "mechanical",
"port": {
"stirrer_1": "stirrer_head",
"main_reactor": "stirrer_port"
}
},
{
"id": "link_stirrer2_secondary_reactor",
"source": "stirrer_2",
"target": "secondary_reactor",
"type": "mechanical",
"port": {
"stirrer_2": "stirrer_head",
"secondary_reactor": "stirrer_port"
}
},
{
"id": "link_heatchill1_main_reactor",
"source": "heatchill_1",
"target": "main_reactor",
"type": "thermal",
"port": {
"heatchill_1": "heating_surface",
"main_reactor": "heating_jacket"
}
},
{
"id": "link_heatchill2_secondary_reactor",
"source": "heatchill_2",
"target": "secondary_reactor",
"type": "thermal",
"port": {
"heatchill_2": "heating_surface",
"secondary_reactor": "heating_jacket"
}
},
{
"id": "link_valve1_ethanol",
"source": "multiway_valve_1",
"target": "flask_ethanol",
"type": "fluid",
"port": {
"multiway_valve_1": "7",
"flask_ethanol": "outlet"
}
}
]
}

View File

@@ -0,0 +1,778 @@
{
"nodes": [
{
"id": "PumpTransferFilterThroughTestStation",
"name": "泵转移+过滤介质测试站",
"children": [
"transfer_pump_1",
"transfer_pump_2",
"multiway_valve_1",
"multiway_valve_2",
"reaction_mixture",
"crude_product",
"filter_celite",
"column_silica_gel",
"filter_C18",
"pure_product",
"collection_bottle_1",
"collection_bottle_2",
"collection_bottle_3",
"intermediate_vessel_1",
"intermediate_vessel_2",
"flask_water",
"flask_ethanol",
"flask_methanol",
"flask_ethyl_acetate",
"flask_acetone",
"flask_hexane",
"flask_air",
"waste_workup"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 500,
"y": 200,
"z": 0
},
"config": {
"protocol_type": [
"PumpTransferProtocol",
"FilterThroughProtocol"
]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "主转移泵",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 200,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_PUMP1",
"max_volume": 25.0,
"transfer_rate": 2.0
},
"data": {
"position": 0.0,
"status": "Idle"
}
},
{
"id": "transfer_pump_2",
"name": "副转移泵",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 400,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_PUMP2",
"max_volume": 25.0,
"transfer_rate": 2.0
},
"data": {
"position": 0.0,
"status": "Idle"
}
},
{
"id": "multiway_valve_1",
"name": "溶剂分配阀",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 200,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_VALVE1",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "multiway_valve_2",
"name": "样品分配阀",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 400,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_VALVE2",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "reaction_mixture",
"name": "反应混合物",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "organic_reaction_mixture",
"liquid_volume": 250.0
}
]
}
},
{
"id": "crude_product",
"name": "粗产品",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 200,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "crude_organic_compound",
"liquid_volume": 150.0
}
]
}
},
{
"id": "filter_celite",
"name": "硅藻土过滤器",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 600,
"y": 450,
"z": 0
},
"config": {
"max_volume": 300.0,
"filter_type": "celite_pad"
},
"data": {
"liquid": []
}
},
{
"id": "column_silica_gel",
"name": "硅胶柱",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 700,
"y": 450,
"z": 0
},
"config": {
"max_volume": 200.0,
"filter_type": "silica_gel_column"
},
"data": {
"liquid": []
}
},
{
"id": "filter_C18",
"name": "C18固相萃取柱",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 800,
"y": 450,
"z": 0
},
"config": {
"max_volume": 100.0,
"filter_type": "C18_cartridge"
},
"data": {
"liquid": []
}
},
{
"id": "pure_product",
"name": "纯产品",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 900,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_bottle_1",
"name": "收集瓶1",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 600,
"y": 550,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_bottle_2",
"name": "收集瓶2",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 700,
"y": 550,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_bottle_3",
"name": "收集瓶3",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 800,
"y": 550,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "intermediate_vessel_1",
"name": "中间容器1",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 300,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "intermediate_vessel_2",
"name": "中间容器2",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_water",
"name": "蒸馏水瓶",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "water",
"liquid_volume": 900.0
}
]
}
},
{
"id": "flask_ethanol",
"name": "乙醇瓶",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 200,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "ethanol",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_methanol",
"name": "甲醇瓶",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 300,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "methanol",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_ethyl_acetate",
"name": "乙酸乙酯瓶",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "ethyl_acetate",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_acetone",
"name": "丙酮瓶",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 500,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "acetone",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_hexane",
"name": "正己烷瓶",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 600,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "hexane",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 700,
"y": 600,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "waste_workup",
"name": "废液瓶",
"children": [],
"parent": "PumpTransferFilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 800,
"y": 600,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"id": "link_pump1_valve1",
"source": "transfer_pump_1",
"target": "multiway_valve_1",
"type": "fluid",
"port": {
"transfer_pump_1": "transferpump",
"multiway_valve_1": "transferpump"
}
},
{
"id": "link_pump2_valve2",
"source": "transfer_pump_2",
"target": "multiway_valve_2",
"type": "fluid",
"port": {
"transfer_pump_2": "transferpump",
"multiway_valve_2": "transferpump"
}
},
{
"id": "link_valve1_air",
"source": "multiway_valve_1",
"target": "flask_air",
"type": "fluid",
"port": {
"multiway_valve_1": "1",
"flask_air": "top"
}
},
{
"id": "link_valve1_water",
"source": "multiway_valve_1",
"target": "flask_water",
"type": "fluid",
"port": {
"multiway_valve_1": "2",
"flask_water": "outlet"
}
},
{
"id": "link_valve1_ethanol",
"source": "multiway_valve_1",
"target": "flask_ethanol",
"type": "fluid",
"port": {
"multiway_valve_1": "3",
"flask_ethanol": "outlet"
}
},
{
"id": "link_valve1_methanol",
"source": "multiway_valve_1",
"target": "flask_methanol",
"type": "fluid",
"port": {
"multiway_valve_1": "4",
"flask_methanol": "outlet"
}
},
{
"id": "link_valve1_ethyl_acetate",
"source": "multiway_valve_1",
"target": "flask_ethyl_acetate",
"type": "fluid",
"port": {
"multiway_valve_1": "5",
"flask_ethyl_acetate": "outlet"
}
},
{
"id": "link_valve1_acetone",
"source": "multiway_valve_1",
"target": "flask_acetone",
"type": "fluid",
"port": {
"multiway_valve_1": "6",
"flask_acetone": "outlet"
}
},
{
"id": "link_valve1_hexane",
"source": "multiway_valve_1",
"target": "flask_hexane",
"type": "fluid",
"port": {
"multiway_valve_1": "7",
"flask_hexane": "outlet"
}
},
{
"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_reaction_mixture",
"source": "multiway_valve_2",
"target": "reaction_mixture",
"type": "fluid",
"port": {
"multiway_valve_2": "2",
"reaction_mixture": "inlet"
}
},
{
"id": "link_valve2_crude_product",
"source": "multiway_valve_2",
"target": "crude_product",
"type": "fluid",
"port": {
"multiway_valve_2": "3",
"crude_product": "inlet"
}
},
{
"id": "link_valve2_intermediate1",
"source": "multiway_valve_2",
"target": "intermediate_vessel_1",
"type": "fluid",
"port": {
"multiway_valve_2": "4",
"intermediate_vessel_1": "inlet"
}
},
{
"id": "link_valve2_intermediate2",
"source": "multiway_valve_2",
"target": "intermediate_vessel_2",
"type": "fluid",
"port": {
"multiway_valve_2": "5",
"intermediate_vessel_2": "inlet"
}
},
{
"id": "link_valve2_celite",
"source": "multiway_valve_2",
"target": "filter_celite",
"type": "fluid",
"port": {
"multiway_valve_2": "6",
"filter_celite": "inlet"
}
},
{
"id": "link_valve2_silica_gel",
"source": "multiway_valve_2",
"target": "column_silica_gel",
"type": "fluid",
"port": {
"multiway_valve_2": "7",
"column_silica_gel": "inlet"
}
},
{
"id": "link_valve2_C18",
"source": "multiway_valve_2",
"target": "filter_C18",
"type": "fluid",
"port": {
"multiway_valve_2": "8",
"filter_C18": "inlet"
}
},
{
"id": "link_celite_collection1",
"source": "filter_celite",
"target": "collection_bottle_1",
"type": "fluid",
"port": {
"filter_celite": "outlet",
"collection_bottle_1": "inlet"
}
},
{
"id": "link_silica_gel_collection2",
"source": "column_silica_gel",
"target": "collection_bottle_2",
"type": "fluid",
"port": {
"column_silica_gel": "outlet",
"collection_bottle_2": "inlet"
}
},
{
"id": "link_C18_collection3",
"source": "filter_C18",
"target": "collection_bottle_3",
"type": "fluid",
"port": {
"filter_C18": "outlet",
"collection_bottle_3": "inlet"
}
},
{
"id": "link_collection1_pure_product",
"source": "collection_bottle_1",
"target": "pure_product",
"type": "fluid",
"port": {
"collection_bottle_1": "outlet",
"pure_product": "inlet"
}
},
{
"id": "link_collection2_pure_product",
"source": "collection_bottle_2",
"target": "pure_product",
"type": "fluid",
"port": {
"collection_bottle_2": "outlet",
"pure_product": "inlet"
}
},
{
"id": "link_collection3_pure_product",
"source": "collection_bottle_3",
"target": "pure_product",
"type": "fluid",
"port": {
"collection_bottle_3": "outlet",
"pure_product": "inlet"
}
},
{
"id": "link_waste_connection",
"source": "pure_product",
"target": "waste_workup",
"type": "fluid",
"port": {
"pure_product": "waste_outlet",
"waste_workup": "inlet"
}
}
]
}

View File

@@ -0,0 +1,304 @@
{
"nodes": [
{
"id": "SimpleProtocolStation",
"name": "简单协议工作站",
"children": [
"transfer_pump_1",
"multiway_valve_1",
"flask_DMF",
"flask_ethyl_acetate",
"flask_methanol",
"main_reactor",
"waste_workup",
"collection_bottle_1",
"flask_air"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 500,
"y": 200,
"z": 0
},
"config": {
"protocol_type": ["PumpTransferProtocol"]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "转移泵1",
"children": [],
"parent": "SimpleProtocolStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 500,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 25.0,
"transfer_rate": 5.0
},
"data": {
"position": 0.0,
"status": "Idle",
"valve_position": "0"
}
},
{
"id": "multiway_valve_1",
"name": "八通阀1",
"children": [],
"parent": "SimpleProtocolStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 500,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "flask_DMF",
"name": "DMF试剂瓶",
"children": [],
"parent": "SimpleProtocolStation",
"type": "container",
"class": null,
"position": {
"x": 200,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "DMF",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_ethyl_acetate",
"name": "乙酸乙酯试剂瓶",
"children": [],
"parent": "SimpleProtocolStation",
"type": "container",
"class": null,
"position": {
"x": 300,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "ethyl_acetate",
"liquid_volume": 800.0
}
]
}
},
{
"id": "flask_methanol",
"name": "甲醇试剂瓶",
"children": [],
"parent": "SimpleProtocolStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"liquid_type": "methanol",
"liquid_volume": 800.0
}
]
}
},
{
"id": "main_reactor",
"name": "主反应器",
"children": [],
"parent": "SimpleProtocolStation",
"type": "container",
"class": null,
"position": {
"x": 600,
"y": 500,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "waste_workup",
"name": "废液处理瓶",
"children": [],
"parent": "SimpleProtocolStation",
"type": "container",
"class": null,
"position": {
"x": 700,
"y": 500,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_bottle_1",
"name": "收集瓶1",
"children": [],
"parent": "SimpleProtocolStation",
"type": "container",
"class": null,
"position": {
"x": 800,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "SimpleProtocolStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"id": "link_pump_valve",
"source": "transfer_pump_1",
"target": "multiway_valve_1",
"type": "fluid",
"port": {
"transfer_pump_1": "transferpump",
"multiway_valve_1": "transferpump"
}
},
{
"id": "link_valve_air",
"source": "multiway_valve_1",
"target": "flask_air",
"type": "fluid",
"port": {
"multiway_valve_1": "1",
"flask_air": "top"
}
},
{
"id": "link_valve_DMF",
"source": "multiway_valve_1",
"target": "flask_DMF",
"type": "fluid",
"port": {
"multiway_valve_1": "2",
"flask_DMF": "outlet"
}
},
{
"id": "link_valve_ethyl_acetate",
"source": "multiway_valve_1",
"target": "flask_ethyl_acetate",
"type": "fluid",
"port": {
"multiway_valve_1": "3",
"flask_ethyl_acetate": "outlet"
}
},
{
"id": "link_valve_methanol",
"source": "multiway_valve_1",
"target": "flask_methanol",
"type": "fluid",
"port": {
"multiway_valve_1": "4",
"flask_methanol": "outlet"
}
},
{
"id": "link_valve_reactor",
"source": "multiway_valve_1",
"target": "main_reactor",
"type": "fluid",
"port": {
"multiway_valve_1": "5",
"main_reactor": "inlet"
}
},
{
"id": "link_valve_waste",
"source": "multiway_valve_1",
"target": "waste_workup",
"type": "fluid",
"port": {
"multiway_valve_1": "6",
"waste_workup": "inlet"
}
},
{
"id": "link_valve_collection",
"source": "multiway_valve_1",
"target": "collection_bottle_1",
"type": "fluid",
"port": {
"multiway_valve_1": "7",
"collection_bottle_1": "inlet"
}
}
]
}

View File

@@ -0,0 +1,432 @@
{
"nodes": [
{
"id": "RunColumnTestStation",
"name": "柱层析测试工作站",
"children": [
"transfer_pump_1",
"multiway_valve_1",
"column_1",
"flask_sample",
"flask_hexane",
"flask_ethyl_acetate",
"flask_methanol",
"column_vessel",
"collection_flask_1",
"collection_flask_2",
"collection_flask_3",
"waste_flask",
"main_reactor"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 500,
"y": 200,
"z": 0
},
"config": {
"protocol_type": ["RunColumnProtocol", "PumpTransferProtocol"]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "转移泵",
"children": [],
"parent": "RunColumnTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 300,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL_PUMP1",
"max_volume": 50.0,
"transfer_rate": 10.0
},
"data": {
"status": "Idle",
"position": 0.0
}
},
{
"id": "multiway_valve_1",
"name": "八通阀门",
"children": [],
"parent": "RunColumnTestStation",
"type": "device",
"class": "virtual_multiway_valve",
"position": {
"x": 300,
"y": 400,
"z": 0
},
"config": {
"port": "VIRTUAL_VALVE1",
"positions": 8
},
"data": {
"current_position": 1
}
},
{
"id": "column_1",
"name": "柱层析设备",
"children": [],
"parent": "RunColumnTestStation",
"type": "device",
"class": "virtual_column",
"position": {
"x": 600,
"y": 350,
"z": 0
},
"config": {
"port": "VIRTUAL_COLUMN1",
"max_flow_rate": 5.0,
"column_length": 30.0,
"column_diameter": 2.5
},
"data": {
"status": "Idle",
"column_state": "Ready"
}
},
{
"id": "flask_sample",
"name": "样品瓶",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 500,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": [
{
"name": "crude_mixture",
"volume": 200.0,
"concentration": 70.0
}
]
}
},
{
"id": "flask_hexane",
"name": "正己烷洗脱剂",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 200,
"y": 500,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "hexane",
"volume": 1500.0,
"concentration": 99.8
}
]
}
},
{
"id": "flask_ethyl_acetate",
"name": "乙酸乙酯洗脱剂",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 300,
"y": 500,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "ethyl_acetate",
"volume": 1500.0,
"concentration": 99.5
}
]
}
},
{
"id": "flask_methanol",
"name": "甲醇洗脱剂",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "methanol",
"volume": 800.0,
"concentration": 99.9
}
]
}
},
{
"id": "column_vessel",
"name": "柱容器",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 600,
"y": 450,
"z": 0
},
"config": {
"max_volume": 300.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_flask_1",
"name": "收集瓶1",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 700,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_flask_2",
"name": "收集瓶2",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 800,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_flask_3",
"name": "收集瓶3",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 900,
"y": 500,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "waste_flask",
"name": "废液瓶",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 1000,
"y": 500,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "main_reactor",
"name": "反应器",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 600,
"y": 300,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "reaction_mixture",
"volume": 300.0,
"concentration": 85.0
}
]
}
}
],
"links": [
{
"id": "link_pump_valve",
"source": "transfer_pump_1",
"target": "multiway_valve_1",
"type": "fluid",
"port": {
"transfer_pump_1": "transferpump",
"multiway_valve_1": "transferpump"
}
},
{
"id": "link_valve_sample",
"source": "multiway_valve_1",
"target": "flask_sample",
"type": "fluid",
"port": {
"multiway_valve_1": "1",
"flask_sample": "outlet"
}
},
{
"id": "link_valve_hexane",
"source": "multiway_valve_1",
"target": "flask_hexane",
"type": "fluid",
"port": {
"multiway_valve_1": "2",
"flask_hexane": "outlet"
}
},
{
"id": "link_valve_ethyl_acetate",
"source": "multiway_valve_1",
"target": "flask_ethyl_acetate",
"type": "fluid",
"port": {
"multiway_valve_1": "3",
"flask_ethyl_acetate": "outlet"
}
},
{
"id": "link_valve_methanol",
"source": "multiway_valve_1",
"target": "flask_methanol",
"type": "fluid",
"port": {
"multiway_valve_1": "4",
"flask_methanol": "outlet"
}
},
{
"id": "link_valve_column_vessel",
"source": "multiway_valve_1",
"target": "column_vessel",
"type": "fluid",
"port": {
"multiway_valve_1": "5",
"column_vessel": "inlet"
}
},
{
"id": "link_valve_collection1",
"source": "multiway_valve_1",
"target": "collection_flask_1",
"type": "fluid",
"port": {
"multiway_valve_1": "6",
"collection_flask_1": "inlet"
}
},
{
"id": "link_valve_collection2",
"source": "multiway_valve_1",
"target": "collection_flask_2",
"type": "fluid",
"port": {
"multiway_valve_1": "7",
"collection_flask_2": "inlet"
}
},
{
"id": "link_valve_waste",
"source": "multiway_valve_1",
"target": "waste_flask",
"type": "fluid",
"port": {
"multiway_valve_1": "8",
"waste_flask": "inlet"
}
},
{
"id": "link_column_device_vessel",
"source": "column_1",
"target": "column_vessel",
"type": "transport",
"port": {
"column_1": "columnin",
"column_vessel": "column_port"
}
},
{
"id": "link_column_collection3",
"source": "column_1",
"target": "collection_flask_3",
"type": "transport",
"port": {
"column_1": "columnout",
"collection_flask_3": "column_outlet"
}
}
]
}

View File

@@ -0,0 +1,141 @@
{
"nodes": [
{
"id": "SimpleStirHeatChillTestStation",
"name": "搅拌加热测试站",
"children": [
"stirrer_1",
"heatchill_1",
"main_reactor",
"secondary_reactor"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 500,
"y": 200,
"z": 0
},
"config": {
"protocol_type": [
"StirProtocol",
"StartStirProtocol",
"StopStirProtocol",
"HeatChillProtocol",
"HeatChillStartProtocol",
"HeatChillStopProtocol"
]
},
"data": {}
},
{
"id": "stirrer_1",
"name": "主搅拌器",
"children": [],
"parent": "SimpleStirHeatChillTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 400,
"y": 350,
"z": 0
},
"config": {
"port": "VIRTUAL_STIRRER1",
"max_speed": 1500.0,
"min_speed": 50.0
},
"data": {
"status": "Idle"
}
},
{
"id": "heatchill_1",
"name": "主加热冷却器",
"children": [],
"parent": "SimpleStirHeatChillTestStation",
"type": "device",
"class": "virtual_heatchill",
"position": {
"x": 600,
"y": 350,
"z": 0
},
"config": {
"port": "VIRTUAL_HEATCHILL1",
"max_temp": 200.0,
"min_temp": -80.0,
"max_stir_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "main_reactor",
"name": "主反应器",
"children": [],
"parent": "SimpleStirHeatChillTestStation",
"type": "container",
"class": null,
"position": {
"x": 500,
"y": 450,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"liquid_type": "water",
"liquid_volume": 500.0
}
]
}
},
{
"id": "secondary_reactor",
"name": "副反应器",
"children": [],
"parent": "SimpleStirHeatChillTestStation",
"type": "container",
"class": null,
"position": {
"x": 700,
"y": 450,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"id": "link_stirrer1_main_reactor",
"source": "stirrer_1",
"target": "main_reactor",
"type": "mechanical",
"port": {
"stirrer_1": "stirrer",
"main_reactor": "stirrer_port"
}
},
{
"id": "link_heatchill1_main_reactor",
"source": "heatchill_1",
"target": "main_reactor",
"type": "mechanical",
"port": {
"heatchill_1": "heatchill",
"main_reactor": "heating_jacket"
}
}
]
}

View File

@@ -0,0 +1,36 @@
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
PumpTransferProtocol: generate_pump_protocol_with_rinsing, (√)
这个重复了删掉CleanProtocol: generate_clean_protocol,
SeparateProtocol: generate_separate_protocol, (×)
EvaporateProtocol: generate_evaporate_protocol, (√)
EvacuateAndRefillProtocol: generate_evacuateandrefill_protocol, (√)
CentrifugeProtocol: generate_centrifuge_protocol, (√)
AddProtocol: generate_add_protocol, (√)
FilterProtocol: generate_filter_protocol, (√)
HeatChillProtocol: generate_heat_chill_protocol, (√)
HeatChillStartProtocol: generate_heat_chill_start_protocol, (√)
HeatChillStopProtocol: generate_heat_chill_stop_protocol, (√)
StirProtocol: generate_stir_protocol, (√)
StartStirProtocol: generate_start_stir_protocol, (√)
StopStirProtocol: generate_stop_stir_protocol, (√)
这个重复了删掉TransferProtocol: generate_transfer_protocol,
CleanVesselProtocol: generate_clean_vessel_protocol, (√)
DissolveProtocol: generate_dissolve_protocol, (√)
FilterThroughProtocol: generate_filter_through_protocol, (√)
RunColumnProtocol: generate_run_column_protocol, (×)
WashSolidProtocol: generate_wash_solid_protocol, (×)
上下文体积搜索

View File

@@ -0,0 +1,897 @@
{
"nodes": [
{
"id": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"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": "OrganicSynthesisStation",
"type": "container",
"class": "container",
"position": {
"x": 1050,
"y": 500,
"z": 0
},
"config": {
"volume": 250.0
},
"data": {
"current_volume": 0.0
}
}
],
"links": [
{
"id": "link_pump1_valve1",
"source": "transfer_pump_1",
"target": "multiway_valve_1",
"type": "fluid",
"port": {
"transfer_pump_1": "transferpump",
"multiway_valve_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": "transport",
"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": "sample_in"
}
},
{
"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_pump2_valve2",
"source": "transfer_pump_2",
"target": "multiway_valve_2",
"type": "fluid",
"port": {
"transfer_pump_2": "transferpump",
"multiway_valve_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_vacuum_solenoid1",
"source": "vacuum_pump_1",
"target": "solenoid_valve_1",
"type": "fluid",
"port": {
"vacuum_pump_1": "vacuumpump",
"solenoid_valve_1": "out"
}
},
{
"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_gas_solenoid2",
"source": "gas_source_1",
"target": "solenoid_valve_2",
"type": "fluid",
"port": {
"gas_source_1": "gassource",
"solenoid_valve_2": "out"
}
},
{
"id": "link_valve2_filter",
"source": "multiway_valve_2",
"target": "filter_1",
"type": "transport",
"port": {
"multiway_valve_2": "4",
"filter_1": "filter_in"
}
},
{
"id": "link_valve2_column",
"source": "multiway_valve_2",
"target": "column_1",
"type": "transport",
"port": {
"multiway_valve_2": "5",
"column_1": "columnin"
}
},
{
"id": "link_column_collection2",
"source": "column_1",
"target": "collection_bottle_2",
"type": "transport",
"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": "separator_in"
}
},
{
"id": "link_separator_collection3",
"source": "separator_1",
"target": "collection_bottle_3",
"type": "fluid",
"port": {
"separator_1": "bottom_phase_out",
"collection_bottle_3": "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": "mechanical",
"port": {
"stirrer_1": "stirrer",
"main_reactor": "bind"
}
},
{
"id": "thermal_heater_reactor",
"source": "heater_1",
"target": "main_reactor",
"type": "mechanical",
"port": {
"heater_1": "heatchill",
"main_reactor": "bind"
}
},
{
"id": "link_separator_waste2",
"source": "separator_1",
"target": "waste_bottle_2",
"type": "fluid",
"port": {
"separator_1": "top_phase_out",
"waste_bottle_2": "top"
}
},
{
"id": "mech_stirrer2_separator",
"source": "stirrer_2",
"target": "separator_1",
"type": "mechanical",
"port": {
"stirrer_2": "stirrer",
"separator_1": "bind"
}
},
{
"id": "link_filter_filtrate_to_collection1",
"source": "filter_1",
"target": "collection_bottle_1",
"type": "transport",
"port": {
"filter_1": "filtrate_out",
"collection_bottle_1": "top"
}
},
{
"id": "link_filter_retentate_to_waste1",
"source": "filter_1",
"target": "waste_bottle_1",
"type": "transport",
"port": {
"filter_1": "retentate_out",
"waste_bottle_1": "top"
}
}
]
}

View File

@@ -0,0 +1,314 @@
{
"nodes": [
{
"id": "MockChiller1",
"name": "模拟冷却器",
"children": [
"MockContainerForChiller1"
],
"parent": null,
"type": "device",
"class": "mock_chiller",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"current_temperature": 25.0,
"target_temperature": 25.0,
"status": "Idle",
"is_cooling": false,
"is_heating": false,
"vessel": "",
"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": "模拟过滤器",
"children": [],
"parent": null,
"type": "device",
"class": "mock_filter",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"status": "Idle",
"is_filtering": false,
"flow_rate": 0.0,
"filter_life": 100.0,
"vessel": "",
"filtrate_vessel": "",
"filtered_volume": 0.0,
"target_volume": 0.0,
"progress": 0.0,
"stir": false,
"stir_speed": 0.0,
"temperature": 25.0,
"continue_heatchill": false
}
},
{
"id": "MockHeater1",
"name": "模拟加热器",
"children": [],
"parent": null,
"type": "device",
"class": "mock_heater",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"current_temperature": 25.0,
"target_temperature": 25.0,
"status": "Idle",
"is_heating": false,
"heating_power": 0.0,
"max_temperature": 300.0,
"vessel": "Unknown",
"purpose": "Unknown",
"stir": false,
"stir_speed": 0.0
}
},
{
"id": "MockPump1",
"name": "模拟泵设备",
"children": [],
"parent": null,
"type": "device",
"class": "mock_pump",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"status": "Idle",
"current_device": "MockPump1",
"pump_state": "Stopped",
"flow_rate": 0.0,
"target_flow_rate": 0.0,
"pressure": 0.0,
"total_volume": 0.0,
"max_flow_rate": 100.0,
"max_pressure": 10.0,
"from_vessel": "",
"to_vessel": "",
"transfer_volume": 0.0,
"amount": "",
"transfer_time": 0.0,
"is_viscous": false,
"rinsing_solvent": "",
"rinsing_volume": 0.0,
"rinsing_repeats": 0,
"is_solid": false,
"time_spent": 0.0,
"time_remaining": 0.0
}
},
{
"id": "MockRotavap1",
"name": "模拟旋转蒸发器",
"children": [],
"parent": null,
"type": "device",
"class": "mock_rotavap",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"status": "Idle",
"rotate_state": "Stopped",
"rotate_time": 0.0,
"rotate_speed": 0.0,
"pump_state": "Stopped",
"pump_time": 0.0,
"vacuum_level": 1013.25,
"temperature": 25.0,
"target_temperature": 25.0,
"success": "True"
}
},
{
"id": "MockSeparator1",
"name": "模拟分离器",
"children": [],
"parent": null,
"type": "device",
"class": "mock_separator",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"status": "Idle",
"settling_time": 0.0,
"valve_state": "Closed",
"shake_time": 0.0,
"shake_status": "Not Shaking",
"current_device": "MockSeparator1",
"purpose": "",
"product_phase": "",
"from_vessel": "",
"separation_vessel": "",
"to_vessel": "",
"waste_phase_to_vessel": "",
"solvent": "",
"solvent_volume": 0.0,
"through": "",
"repeats": 1,
"stir_time": 0.0,
"stir_speed": 0.0,
"time_spent": 0.0,
"time_remaining": 0.0
}
},
{
"id": "MockSolenoidValve1",
"name": "模拟电磁阀",
"children": [],
"parent": null,
"type": "device",
"class": "mock_solenoid_valve",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"status": "Idle",
"valve_status": "Closed"
}
},
{
"id": "MockStirrer1NEW",
"name": "模拟搅拌器(new)",
"children": [],
"parent": null,
"type": "device",
"class": "mock_stirrer_new",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"status": "Idle",
"vessel": "",
"purpose": "",
"stir_speed": 0.0,
"target_stir_speed": 0.0,
"stir_state": "Stopped",
"stir_time": 0.0,
"settling_time": 0.0,
"progress": 0.0,
"max_stir_speed": 2000.0
}
},
{
"id": "MockStirrer1",
"name": "模拟搅拌器",
"children": [],
"parent": null,
"type": "device",
"class": "mock_stirrer",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"status": "Idle",
"stir_speed": 0.0,
"target_stir_speed": 0.0,
"stir_state": "Stopped",
"temperature": 25.0,
"target_temperature": 25.0,
"heating_state": "Off",
"heating_power": 0.0,
"max_stir_speed": 2000.0,
"max_temperature": 300.0
}
},
{
"id": "MockVacuum1",
"name": "模拟真空泵",
"children": [],
"parent": null,
"type": "device",
"class": "mock_vacuum",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"status": "Idle",
"power_state": "Off",
"pump_state": "Stopped",
"vacuum_level": 1013.25,
"target_vacuum": 50.0,
"pump_speed": 0.0,
"pump_efficiency": 95.0,
"max_pump_speed": 100.0
}
}
],
"links": []
}

View File

@@ -0,0 +1,30 @@
{
"nodes": [
{
"id": "MockChiller1",
"name": "模拟冷却器",
"children": [],
"parent": null,
"type": "device",
"class": "mock_chiller",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"current_temperature": 25.0,
"target_temperature": 25.0,
"status": "Idle",
"is_cooling": false,
"is_heating": false,
"vessel": "",
"purpose": ""
}
}
],
"links": []
}

View File

@@ -0,0 +1,36 @@
{
"nodes": [
{
"id": "MockFilter1",
"name": "模拟过滤器",
"children": [],
"parent": null,
"type": "device",
"class": "mock_filter",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"status": "Idle",
"is_filtering": false,
"flow_rate": 0.0,
"filter_life": 100.0,
"vessel": "",
"filtrate_vessel": "",
"filtered_volume": 0.0,
"target_volume": 0.0,
"progress": 0.0,
"stir": false,
"stir_speed": 0.0,
"temperature": 25.0,
"continue_heatchill": false
}
}
],
"links": []
}

View File

@@ -0,0 +1,33 @@
{
"nodes": [
{
"id": "MockHeater1",
"name": "模拟加热器",
"children": [],
"parent": null,
"type": "device",
"class": "mock_heater",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"current_temperature": 25.0,
"target_temperature": 25.0,
"status": "Idle",
"is_heating": false,
"heating_power": 0.0,
"max_temperature": 300.0,
"vessel": "Unknown",
"purpose": "Unknown",
"stir": false,
"stir_speed": 0.0
}
}
],
"links": []
}

View File

@@ -0,0 +1,44 @@
{
"nodes": [
{
"id": "MockPump1",
"name": "模拟泵设备",
"children": [],
"parent": null,
"type": "device",
"class": "mock_pump",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"status": "Idle",
"current_device": "MockPump1",
"pump_state": "Stopped",
"flow_rate": 0.0,
"target_flow_rate": 0.0,
"pressure": 0.0,
"total_volume": 0.0,
"max_flow_rate": 100.0,
"max_pressure": 10.0,
"from_vessel": "",
"to_vessel": "",
"transfer_volume": 0.0,
"amount": "",
"transfer_time": 0.0,
"is_viscous": false,
"rinsing_solvent": "",
"rinsing_volume": 0.0,
"rinsing_repeats": 0,
"is_solid": false,
"time_spent": 0.0,
"time_remaining": 0.0
}
}
],
"links": []
}

View File

@@ -0,0 +1,33 @@
{
"nodes": [
{
"id": "MockRotavap1",
"name": "模拟旋转蒸发器",
"children": [],
"parent": null,
"type": "device",
"class": "mock_rotavap",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"status": "Idle",
"rotate_state": "Stopped",
"rotate_time": 0.0,
"rotate_speed": 0.0,
"pump_state": "Stopped",
"pump_time": 0.0,
"vacuum_level": 1013.25,
"temperature": 25.0,
"target_temperature": 25.0,
"success": "True"
}
}
],
"links": []
}

View File

@@ -0,0 +1,43 @@
{
"nodes": [
{
"id": "MockSeparator1",
"name": "模拟分离器",
"children": [],
"parent": null,
"type": "device",
"class": "mock_separator",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"status": "Idle",
"settling_time": 0.0,
"valve_state": "Closed",
"shake_time": 0.0,
"shake_status": "Not Shaking",
"current_device": "MockSeparator1",
"purpose": "",
"product_phase": "",
"from_vessel": "",
"separation_vessel": "",
"to_vessel": "",
"waste_phase_to_vessel": "",
"solvent": "",
"solvent_volume": 0.0,
"through": "",
"repeats": 1,
"stir_time": 0.0,
"stir_speed": 0.0,
"time_spent": 0.0,
"time_remaining": 0.0
}
}
],
"links": []
}

View File

@@ -0,0 +1,25 @@
{
"nodes": [
{
"id": "MockSolenoidValve1",
"name": "模拟电磁阀",
"children": [],
"parent": null,
"type": "device",
"class": "mock_solenoid_valve",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"status": "Idle",
"valve_status": "Closed"
}
}
],
"links": []
}

View File

@@ -0,0 +1,33 @@
{
"nodes": [
{
"id": "MockStirrer1",
"name": "模拟搅拌器",
"children": [],
"parent": null,
"type": "device",
"class": "mock_stirrer",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"status": "Idle",
"stir_speed": 0.0,
"target_stir_speed": 0.0,
"stir_state": "Stopped",
"temperature": 25.0,
"target_temperature": 25.0,
"heating_state": "Off",
"heating_power": 0.0,
"max_stir_speed": 2000.0,
"max_temperature": 300.0
}
}
],
"links": []
}

View File

@@ -0,0 +1,33 @@
{
"nodes": [
{
"id": "MockStirrer1COPY",
"name": "模拟搅拌器(Copy)",
"children": [],
"parent": null,
"type": "device",
"class": "mock_stirrer_new",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"status": "Idle",
"vessel": "",
"purpose": "",
"stir_speed": 0.0,
"target_stir_speed": 0.0,
"stir_state": "Stopped",
"stir_time": 0.0,
"settling_time": 0.0,
"progress": 0.0,
"max_stir_speed": 2000.0
}
}
],
"links": []
}

View File

@@ -0,0 +1,31 @@
{
"nodes": [
{
"id": "MockVacuum1",
"name": "模拟真空泵",
"children": [],
"parent": null,
"type": "device",
"class": "mock_vacuum",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"port": "MOCK"
},
"data": {
"status": "Idle",
"power_state": "Off",
"pump_state": "Stopped",
"vacuum_level": 1013.25,
"target_vacuum": 50.0,
"pump_speed": 0.0,
"pump_efficiency": 95.0,
"max_pump_speed": 100.0
}
}
],
"links": []
}

View File

@@ -0,0 +1,376 @@
{
"nodes": [
{
"id": "AddTestStation",
"name": "添加试剂测试工作站",
"children": [
"transfer_pump",
"multiway_valve",
"stirrer",
"flask_reagent1",
"flask_reagent2",
"flask_reagent3",
"flask_reagent4",
"reactor",
"flask_waste",
"flask_rinsing",
"flask_buffer"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["AddProtocol", "TransferProtocol", "StartStirProtocol", "StopStirProtocol"]
},
"data": {}
},
{
"id": "transfer_pump",
"name": "注射器泵",
"children": [],
"parent": "AddTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 520,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"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": "搅拌器",
"children": [],
"parent": "AddTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 720,
"y": 450,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 100.0,
"max_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "flask_reagent1",
"name": "试剂瓶1 (甲醇)",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 400,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "甲醇",
"volume": 800.0,
"concentration": "99.9%"
}
]
}
},
{
"id": "flask_reagent2",
"name": "试剂瓶2 (乙醇)",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 180,
"y": 400,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "乙醇",
"volume": 750.0,
"concentration": "95%"
}
]
}
},
{
"id": "flask_reagent3",
"name": "试剂瓶3 (丙酮)",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 260,
"y": 400,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "丙酮",
"volume": 900.0,
"concentration": "99.5%"
}
]
}
},
{
"id": "flask_reagent4",
"name": "试剂瓶4 (二氯甲烷)",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 340,
"y": 400,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "二氯甲烷",
"volume": 850.0,
"concentration": "99.8%"
}
]
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "AddTestStation",
"type": "container",
"class": null,
"position": {
"x": 720,
"y": 400,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"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": "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": {
"multiway_valve": "multiway-valve-port-5",
"reactor": "top"
}
},
{
"source": "multiway_valve",
"target": "flask_waste",
"type": "physical",
"port": {
"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

@@ -0,0 +1,271 @@
{
"nodes": [
{
"id": "CentrifugeTestStation",
"name": "离心机测试工作站",
"children": [
"pump_add",
"flask_1",
"flask_2",
"flask_3",
"reactor",
"stirrer",
"centrifuge_1",
"flask_air"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol", "CentrifugeProtocol"]
},
"data": {}
},
{
"id": "pump_add",
"name": "pump_add",
"children": [],
"parent": "CentrifugeTestStation",
"type": "device",
"class": "virtual_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 25.0
},
"data": {
"status": "Idle"
}
},
{
"id": "stirrer",
"name": "stirrer",
"children": [],
"parent": "CentrifugeTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 650.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 100.0,
"max_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "centrifuge_1",
"name": "离心机",
"children": [],
"parent": "CentrifugeTestStation",
"type": "device",
"class": "virtual_centrifuge",
"position": {
"x": 800,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_speed": 15000.0,
"max_temp": 40.0,
"min_temp": 4.0
},
"data": {
"status": "Idle"
}
},
{
"id": "flask_1",
"name": "样品瓶1",
"children": [],
"parent": "CentrifugeTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1500.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_2",
"name": "样品瓶2",
"children": [],
"parent": "CentrifugeTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1500.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_3",
"name": "缓冲液瓶",
"children": [],
"parent": "CentrifugeTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "CentrifugeTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 5000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "CentrifugeTestStation",
"type": "container",
"class": null,
"position": {
"x": 950,
"y": 300,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"source": "stirrer",
"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": "reactor",
"type": "physical",
"port": {
"pump_add": "outlet",
"reactor": "top"
}
},
{
"source": "pump_add",
"target": "flask_air",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_air": "top"
}
},
{
"source": "centrifuge_1",
"target": "reactor",
"type": "logical",
"port": {
"centrifuge_1": "chamber",
"reactor": "vessel"
}
},
{
"source": "centrifuge_1",
"target": "flask_1",
"type": "logical",
"port": {
"centrifuge_1": "chamber",
"flask_1": "vessel"
}
},
{
"source": "centrifuge_1",
"target": "flask_2",
"type": "logical",
"port": {
"centrifuge_1": "chamber",
"flask_2": "vessel"
}
}
]
}

View File

@@ -0,0 +1,362 @@
{
"nodes": [
{
"id": "CleanVesselTestStation",
"name": "容器清洗测试工作站",
"children": [
"transfer_pump_cleaner",
"heatchill_1",
"flask_water",
"flask_ethanol",
"flask_acetone",
"flask_waste",
"reactor",
"flask_buffer",
"flask_sample",
"flask_air"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["CleanVesselProtocol", "TransferProtocol", "AddProtocol"]
},
"data": {}
},
{
"id": "transfer_pump_cleaner",
"name": "清洗转移泵",
"children": [],
"parent": "CleanVesselTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 50.0,
"transfer_rate": 10.0
},
"data": {
"status": "Idle",
"current_volume": 0.0,
"max_volume": 50.0,
"transfer_rate": 10.0,
"from_vessel": "",
"to_vessel": "",
"progress": 0.0,
"transferred_volume": 0.0,
"current_status": "Ready"
}
},
{
"id": "heatchill_1",
"name": "加热冷却器",
"children": [],
"parent": "CleanVesselTestStation",
"type": "device",
"class": "virtual_heatchill",
"position": {
"x": 650.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 150.0,
"min_temp": -20.0
},
"data": {
"status": "Idle",
"current_temp": 25.0,
"target_temp": 25.0,
"vessel": "",
"purpose": "",
"progress": 0.0,
"current_status": "Ready"
}
},
{
"id": "flask_water",
"name": "水溶剂瓶",
"children": [],
"parent": "CleanVesselTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "water",
"volume": 1500.0,
"concentration": 100.0
}
]
}
},
{
"id": "flask_ethanol",
"name": "乙醇溶剂瓶",
"children": [],
"parent": "CleanVesselTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "ethanol",
"volume": 1500.0,
"concentration": 99.5
}
]
}
},
{
"id": "flask_acetone",
"name": "丙酮溶剂瓶",
"children": [],
"parent": "CleanVesselTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "acetone",
"volume": 1800.0,
"concentration": 99.9
}
]
}
},
{
"id": "flask_waste",
"name": "废液瓶",
"children": [],
"parent": "CleanVesselTestStation",
"type": "container",
"class": null,
"position": {
"x": 550,
"y": 428,
"z": 0
},
"config": {
"max_volume": 5000.0
},
"data": {
"liquid": []
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "CleanVesselTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "residue",
"volume": 50.0,
"concentration": 100.0
}
]
}
},
{
"id": "flask_buffer",
"name": "缓冲液瓶",
"children": [],
"parent": "CleanVesselTestStation",
"type": "container",
"class": null,
"position": {
"x": 850,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "buffer",
"volume": 1000.0,
"concentration": 10.0
}
]
}
},
{
"id": "flask_sample",
"name": "样品瓶",
"children": [],
"parent": "CleanVesselTestStation",
"type": "container",
"class": null,
"position": {
"x": 1000,
"y": 428,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "CleanVesselTestStation",
"type": "container",
"class": null,
"position": {
"x": 950,
"y": 300,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"source": "transfer_pump_cleaner",
"target": "flask_water",
"type": "physical",
"port": {
"transfer_pump_cleaner": "1",
"flask_water": "top"
}
},
{
"source": "transfer_pump_cleaner",
"target": "flask_ethanol",
"type": "physical",
"port": {
"transfer_pump_cleaner": "2",
"flask_ethanol": "top"
}
},
{
"source": "transfer_pump_cleaner",
"target": "flask_acetone",
"type": "physical",
"port": {
"transfer_pump_cleaner": "3",
"flask_acetone": "top"
}
},
{
"source": "transfer_pump_cleaner",
"target": "flask_waste",
"type": "physical",
"port": {
"transfer_pump_cleaner": "4",
"flask_waste": "top"
}
},
{
"source": "transfer_pump_cleaner",
"target": "reactor",
"type": "physical",
"port": {
"transfer_pump_cleaner": "5",
"reactor": "top"
}
},
{
"source": "transfer_pump_cleaner",
"target": "flask_buffer",
"type": "physical",
"port": {
"transfer_pump_cleaner": "6",
"flask_buffer": "top"
}
},
{
"source": "transfer_pump_cleaner",
"target": "flask_sample",
"type": "physical",
"port": {
"transfer_pump_cleaner": "7",
"flask_sample": "top"
}
},
{
"source": "transfer_pump_cleaner",
"target": "flask_air",
"type": "physical",
"port": {
"transfer_pump_cleaner": "8",
"flask_air": "top"
}
},
{
"source": "heatchill_1",
"target": "reactor",
"type": "physical",
"port": {
"heatchill_1": "heating_element",
"reactor": "bottom"
}
},
{
"source": "heatchill_1",
"target": "flask_sample",
"type": "physical",
"port": {
"heatchill_1": "heating_element",
"flask_sample": "bottom"
}
}
]
}

View File

@@ -0,0 +1,343 @@
{
"nodes": [
{
"id": "DissolveTestStation",
"name": "溶解测试工作站",
"children": [
"transfer_pump_1",
"heatchill_1",
"stirrer_1",
"flask_water",
"flask_ethanol",
"flask_dmso",
"reactor",
"flask_sample",
"flask_buffer"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["DissolveProtocol", "TransferProtocol", "HeatChillProtocol", "StirProtocol"]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "转移泵",
"children": [],
"parent": "DissolveTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 50.0,
"transfer_rate": 10.0
},
"data": {
"status": "Idle",
"current_volume": 0.0,
"max_volume": 50.0,
"transfer_rate": 10.0,
"from_vessel": "",
"to_vessel": "",
"progress": 0.0,
"transferred_volume": 0.0,
"current_status": "Ready"
}
},
{
"id": "heatchill_1",
"name": "加热冷却器",
"children": [],
"parent": "DissolveTestStation",
"type": "device",
"class": "virtual_heatchill",
"position": {
"x": 650.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 150.0,
"min_temp": -20.0
},
"data": {
"status": "Idle",
"current_temp": 25.0,
"target_temp": 25.0,
"vessel": "",
"purpose": "",
"progress": 0.0,
"current_status": "Ready"
}
},
{
"id": "stirrer_1",
"name": "搅拌器",
"children": [],
"parent": "DissolveTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 750.1111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "flask_water",
"name": "水溶剂瓶",
"children": [],
"parent": "DissolveTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "water",
"volume": 1500.0,
"concentration": 100.0
}
]
}
},
{
"id": "flask_ethanol",
"name": "乙醇溶剂瓶",
"children": [],
"parent": "DissolveTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "ethanol",
"volume": 1500.0,
"concentration": 99.5
}
]
}
},
{
"id": "flask_dmso",
"name": "DMSO溶剂瓶",
"children": [],
"parent": "DissolveTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "dmso",
"volume": 800.0,
"concentration": 99.9
}
]
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "DissolveTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "solid_sample",
"volume": 10.0,
"concentration": 100.0
}
]
}
},
{
"id": "flask_sample",
"name": "样品瓶",
"children": [],
"parent": "DissolveTestStation",
"type": "container",
"class": null,
"position": {
"x": 1000,
"y": 428,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_buffer",
"name": "缓冲液瓶",
"children": [],
"parent": "DissolveTestStation",
"type": "container",
"class": null,
"position": {
"x": 850,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "buffer",
"volume": 1000.0,
"concentration": 10.0
}
]
}
}
],
"links": [
{
"source": "transfer_pump_1",
"target": "flask_water",
"type": "physical",
"port": {
"transfer_pump_1": "1",
"flask_water": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_ethanol",
"type": "physical",
"port": {
"transfer_pump_1": "2",
"flask_ethanol": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_dmso",
"type": "physical",
"port": {
"transfer_pump_1": "3",
"flask_dmso": "top"
}
},
{
"source": "transfer_pump_1",
"target": "reactor",
"type": "physical",
"port": {
"transfer_pump_1": "4",
"reactor": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_sample",
"type": "physical",
"port": {
"transfer_pump_1": "5",
"flask_sample": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_buffer",
"type": "physical",
"port": {
"transfer_pump_1": "6",
"flask_buffer": "top"
}
},
{
"source": "heatchill_1",
"target": "reactor",
"type": "physical",
"port": {
"heatchill_1": "heating_element",
"reactor": "bottom"
}
},
{
"source": "heatchill_1",
"target": "flask_sample",
"type": "physical",
"port": {
"heatchill_1": "heating_element",
"flask_sample": "bottom"
}
},
{
"source": "stirrer_1",
"target": "reactor",
"type": "physical",
"port": {
"stirrer_1": "stir_rod",
"reactor": "center"
}
},
{
"source": "stirrer_1",
"target": "flask_sample",
"type": "physical",
"port": {
"stirrer_1": "stir_rod",
"flask_sample": "center"
}
}
]
}

View File

@@ -0,0 +1,270 @@
{
"nodes": [
{
"id": "FilterTestStation",
"name": "过滤器测试工作站",
"children": [
"pump_add",
"flask_sample",
"flask_filtrate",
"flask_buffer",
"reactor",
"stirrer",
"filter_1",
"flask_air"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol", "FilterProtocol"]
},
"data": {}
},
{
"id": "pump_add",
"name": "pump_add",
"children": [],
"parent": "FilterTestStation",
"type": "device",
"class": "virtual_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 25.0
},
"data": {
"status": "Idle"
}
},
{
"id": "stirrer",
"name": "stirrer",
"children": [],
"parent": "FilterTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 650.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 100.0,
"max_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "filter_1",
"name": "过滤器",
"children": [],
"parent": "FilterTestStation",
"type": "device",
"class": "virtual_filter",
"position": {
"x": 800,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 100.0,
"max_stir_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "flask_sample",
"name": "样品瓶",
"children": [],
"parent": "FilterTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_filtrate",
"name": "滤液瓶",
"children": [],
"parent": "FilterTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_buffer",
"name": "缓冲液瓶",
"children": [],
"parent": "FilterTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "FilterTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 5000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "FilterTestStation",
"type": "container",
"class": null,
"position": {
"x": 950,
"y": 300,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"source": "stirrer",
"target": "reactor",
"type": "physical",
"port": {
"stirrer": "top",
"reactor": "bottom"
}
},
{
"source": "pump_add",
"target": "flask_sample",
"type": "physical",
"port": {
"pump_add": "outlet",
"flask_sample": "top"
}
},
{
"source": "pump_add",
"target": "flask_filtrate",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_filtrate": "top"
}
},
{
"source": "pump_add",
"target": "flask_buffer",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_buffer": "top"
}
},
{
"source": "pump_add",
"target": "reactor",
"type": "physical",
"port": {
"pump_add": "outlet",
"reactor": "top"
}
},
{
"source": "pump_add",
"target": "flask_air",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_air": "top"
}
},
{
"source": "filter_1",
"target": "reactor",
"type": "logical",
"port": {
"filter_1": "input",
"reactor": "vessel"
}
},
{
"source": "filter_1",
"target": "flask_sample",
"type": "logical",
"port": {
"filter_1": "input",
"flask_sample": "vessel"
}
},
{
"source": "filter_1",
"target": "flask_filtrate",
"type": "logical",
"port": {
"filter_1": "output",
"flask_filtrate": "vessel"
}
}
]
}

View File

@@ -0,0 +1,388 @@
{
"nodes": [
{
"id": "FilterThroughTestStation",
"name": "过滤通过测试工作站",
"children": [
"transfer_pump_1",
"filter_1",
"flask_ethanol",
"flask_water",
"flask_methanol",
"reactor",
"collection_flask",
"waste_flask",
"flask_sample",
"flask_celite",
"flask_silica"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["FilterThroughProtocol", "TransferProtocol", "FilterProtocol"]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "转移泵",
"children": [],
"parent": "FilterThroughTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 50.0,
"transfer_rate": 10.0
},
"data": {
"status": "Idle",
"current_volume": 0.0,
"max_volume": 50.0,
"transfer_rate": 10.0,
"from_vessel": "",
"to_vessel": "",
"progress": 0.0,
"transferred_volume": 0.0,
"current_status": "Ready"
}
},
{
"id": "filter_1",
"name": "过滤器",
"children": [],
"parent": "FilterThroughTestStation",
"type": "device",
"class": "virtual_filter",
"position": {
"x": 650.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 100.0,
"max_stir_speed": 1000.0
},
"data": {
"status": "Idle",
"filter_state": "Ready",
"current_temp": 25.0,
"target_temp": 25.0,
"max_temp": 100.0,
"stir_speed": 0.0,
"max_stir_speed": 1000.0,
"filtered_volume": 0.0,
"progress": 0.0,
"message": ""
}
},
{
"id": "flask_ethanol",
"name": "乙醇溶剂瓶",
"children": [],
"parent": "FilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "ethanol",
"volume": 1500.0,
"concentration": 99.5
}
]
}
},
{
"id": "flask_water",
"name": "水溶剂瓶",
"children": [],
"parent": "FilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "water",
"volume": 1800.0,
"concentration": 100.0
}
]
}
},
{
"id": "flask_methanol",
"name": "甲醇溶剂瓶",
"children": [],
"parent": "FilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "methanol",
"volume": 800.0,
"concentration": 99.9
}
]
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "FilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "crude_product",
"volume": 200.0,
"concentration": 80.0
}
]
}
},
{
"id": "collection_flask",
"name": "收集瓶",
"children": [],
"parent": "FilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 850,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "waste_flask",
"name": "废液瓶",
"children": [],
"parent": "FilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 1000,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_sample",
"name": "样品瓶",
"children": [],
"parent": "FilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 550,
"y": 300,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": [
{
"name": "sample_mixture",
"volume": 100.0,
"concentration": 50.0
}
]
}
},
{
"id": "flask_celite",
"name": "硅藻土容器",
"children": [],
"parent": "FilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 150,
"y": 300,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": [
{
"name": "celite",
"volume": 50.0,
"concentration": 100.0
}
]
}
},
{
"id": "flask_silica",
"name": "硅胶容器",
"children": [],
"parent": "FilterThroughTestStation",
"type": "container",
"class": null,
"position": {
"x": 300,
"y": 300,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": [
{
"name": "silica",
"volume": 30.0,
"concentration": 100.0
}
]
}
}
],
"links": [
{
"source": "transfer_pump_1",
"target": "flask_ethanol",
"type": "physical",
"port": {
"transfer_pump_1": "1",
"flask_ethanol": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_water",
"type": "physical",
"port": {
"transfer_pump_1": "2",
"flask_water": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_methanol",
"type": "physical",
"port": {
"transfer_pump_1": "3",
"flask_methanol": "top"
}
},
{
"source": "transfer_pump_1",
"target": "reactor",
"type": "physical",
"port": {
"transfer_pump_1": "4",
"reactor": "top"
}
},
{
"source": "transfer_pump_1",
"target": "collection_flask",
"type": "physical",
"port": {
"transfer_pump_1": "5",
"collection_flask": "top"
}
},
{
"source": "transfer_pump_1",
"target": "waste_flask",
"type": "physical",
"port": {
"transfer_pump_1": "6",
"waste_flask": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_sample",
"type": "physical",
"port": {
"transfer_pump_1": "7",
"flask_sample": "top"
}
},
{
"source": "filter_1",
"target": "collection_flask",
"type": "physical",
"port": {
"filter_1": "filter_element",
"collection_flask": "top"
}
},
{
"source": "filter_1",
"target": "reactor",
"type": "physical",
"port": {
"filter_1": "filter_element",
"reactor": "top"
}
}
]
}

View File

@@ -0,0 +1,262 @@
{
"nodes": [
{
"id": "HeatChillTestStation",
"name": "加热冷却测试工作站",
"children": [
"pump_add",
"flask_sample",
"flask_buffer1",
"flask_buffer2",
"reactor",
"stirrer",
"heatchill_1",
"flask_air"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol", "HeatChillProtocol", "HeatChillStartProtocol", "HeatChillStopProtocol"]
},
"data": {}
},
{
"id": "pump_add",
"name": "pump_add",
"children": [],
"parent": "HeatChillTestStation",
"type": "device",
"class": "virtual_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 25.0
},
"data": {
"status": "Idle"
}
},
{
"id": "stirrer",
"name": "stirrer",
"children": [],
"parent": "HeatChillTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 650.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 100.0,
"max_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "heatchill_1",
"name": "加热冷却器",
"children": [],
"parent": "HeatChillTestStation",
"type": "device",
"class": "virtual_heatchill",
"position": {
"x": 800,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 200.0,
"min_temp": -80.0,
"max_stir_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "flask_sample",
"name": "样品瓶",
"children": [],
"parent": "HeatChillTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_buffer1",
"name": "缓冲液瓶1",
"children": [],
"parent": "HeatChillTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_buffer2",
"name": "缓冲液瓶2",
"children": [],
"parent": "HeatChillTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "HeatChillTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 5000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "HeatChillTestStation",
"type": "container",
"class": null,
"position": {
"x": 950,
"y": 300,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"source": "stirrer",
"target": "reactor",
"type": "physical",
"port": {
"stirrer": "top",
"reactor": "bottom"
}
},
{
"source": "pump_add",
"target": "flask_sample",
"type": "physical",
"port": {
"pump_add": "outlet",
"flask_sample": "top"
}
},
{
"source": "pump_add",
"target": "flask_buffer1",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_buffer1": "top"
}
},
{
"source": "pump_add",
"target": "flask_buffer2",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_buffer2": "top"
}
},
{
"source": "pump_add",
"target": "reactor",
"type": "physical",
"port": {
"pump_add": "outlet",
"reactor": "top"
}
},
{
"source": "pump_add",
"target": "flask_air",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_air": "top"
}
},
{
"source": "heatchill_1",
"target": "reactor",
"type": "logical",
"port": {
"heatchill_1": "heating_element",
"reactor": "vessel"
}
},
{
"source": "heatchill_1",
"target": "flask_sample",
"type": "logical",
"port": {
"heatchill_1": "heating_element",
"flask_sample": "vessel"
}
}
]
}

View File

@@ -0,0 +1,412 @@
{
"nodes": [
{
"id": "RunColumnTestStation",
"name": "柱层析测试工作站",
"children": [
"transfer_pump_1",
"column_1",
"flask_sample",
"flask_hexane",
"flask_ethyl_acetate",
"flask_methanol",
"collection_flask_1",
"collection_flask_2",
"collection_flask_3",
"waste_flask",
"reactor"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["RunColumnProtocol", "TransferProtocol"]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "转移泵",
"children": [],
"parent": "RunColumnTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 50.0,
"transfer_rate": 10.0
},
"data": {
"status": "Idle",
"current_volume": 0.0,
"max_volume": 50.0,
"transfer_rate": 10.0,
"from_vessel": "",
"to_vessel": "",
"progress": 0.0,
"transferred_volume": 0.0,
"current_status": "Ready"
}
},
{
"id": "column_1",
"name": "柱层析设备",
"children": [],
"parent": "RunColumnTestStation",
"type": "device",
"class": "virtual_column",
"position": {
"x": 650.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_flow_rate": 5.0,
"column_length": 30.0,
"column_diameter": 2.5
},
"data": {
"status": "Idle",
"column_state": "Ready",
"current_flow_rate": 0.0,
"max_flow_rate": 5.0,
"column_length": 30.0,
"column_diameter": 2.5,
"processed_volume": 0.0,
"progress": 0.0,
"current_status": "Ready"
}
},
{
"id": "flask_sample",
"name": "样品瓶",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": [
{
"name": "crude_mixture",
"volume": 200.0,
"concentration": 70.0
}
]
}
},
{
"id": "flask_hexane",
"name": "正己烷洗脱剂",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "hexane",
"volume": 1500.0,
"concentration": 99.8
}
]
}
},
{
"id": "flask_ethyl_acetate",
"name": "乙酸乙酯洗脱剂",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "ethyl_acetate",
"volume": 1500.0,
"concentration": 99.5
}
]
}
},
{
"id": "flask_methanol",
"name": "甲醇洗脱剂",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 550,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "methanol",
"volume": 800.0,
"concentration": 99.9
}
]
}
},
{
"id": "collection_flask_1",
"name": "收集瓶1",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 750,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_flask_2",
"name": "收集瓶2",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 900,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "collection_flask_3",
"name": "收集瓶3",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 1050,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "waste_flask",
"name": "废液瓶",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 1200,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "RunColumnTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 300,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "reaction_mixture",
"volume": 300.0,
"concentration": 85.0
}
]
}
}
],
"links": [
{
"source": "transfer_pump_1",
"target": "flask_sample",
"type": "physical",
"port": {
"transfer_pump_1": "1",
"flask_sample": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_hexane",
"type": "physical",
"port": {
"transfer_pump_1": "2",
"flask_hexane": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_ethyl_acetate",
"type": "physical",
"port": {
"transfer_pump_1": "3",
"flask_ethyl_acetate": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_methanol",
"type": "physical",
"port": {
"transfer_pump_1": "4",
"flask_methanol": "top"
}
},
{
"source": "transfer_pump_1",
"target": "column_1",
"type": "physical",
"port": {
"transfer_pump_1": "5",
"column_1": "inlet"
}
},
{
"source": "transfer_pump_1",
"target": "collection_flask_1",
"type": "physical",
"port": {
"transfer_pump_1": "6",
"collection_flask_1": "top"
}
},
{
"source": "transfer_pump_1",
"target": "collection_flask_2",
"type": "physical",
"port": {
"transfer_pump_1": "7",
"collection_flask_2": "top"
}
},
{
"source": "transfer_pump_1",
"target": "collection_flask_3",
"type": "physical",
"port": {
"transfer_pump_1": "8",
"collection_flask_3": "top"
}
},
{
"source": "transfer_pump_1",
"target": "waste_flask",
"type": "physical",
"port": {
"transfer_pump_1": "9",
"waste_flask": "top"
}
},
{
"source": "transfer_pump_1",
"target": "reactor",
"type": "physical",
"port": {
"transfer_pump_1": "10",
"reactor": "top"
}
},
{
"source": "column_1",
"target": "collection_flask_1",
"type": "physical",
"port": {
"column_1": "outlet",
"collection_flask_1": "top"
}
},
{
"source": "column_1",
"target": "collection_flask_2",
"type": "physical",
"port": {
"column_1": "outlet",
"collection_flask_2": "top"
}
},
{
"source": "column_1",
"target": "collection_flask_3",
"type": "physical",
"port": {
"column_1": "outlet",
"collection_flask_3": "top"
}
}
]
}

View File

@@ -0,0 +1,250 @@
{
"nodes": [
{
"id": "StirTestStation",
"name": "搅拌测试工作站",
"children": [
"pump_add",
"flask_sample",
"flask_buffer1",
"flask_buffer2",
"reactor",
"stirrer",
"flask_waste",
"flask_air"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["AddProtocol", "PumpTransferProtocol", "CleanProtocol", "StirProtocol", "StartStirProtocol", "StopStirProtocol"]
},
"data": {}
},
{
"id": "pump_add",
"name": "添加泵",
"children": [],
"parent": "StirTestStation",
"type": "device",
"class": "virtual_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 25.0
},
"data": {
"status": "Idle"
}
},
{
"id": "stirrer",
"name": "搅拌器",
"children": [],
"parent": "StirTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 650.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 100.0,
"max_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "flask_sample",
"name": "样品瓶",
"children": [],
"parent": "StirTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_buffer1",
"name": "缓冲液瓶1",
"children": [],
"parent": "StirTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_buffer2",
"name": "缓冲液瓶2",
"children": [],
"parent": "StirTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "StirTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 5000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_waste",
"name": "废液瓶",
"children": [],
"parent": "StirTestStation",
"type": "container",
"class": null,
"position": {
"x": 850,
"y": 428,
"z": 0
},
"config": {
"max_volume": 3000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_air",
"name": "空气瓶",
"children": [],
"parent": "StirTestStation",
"type": "container",
"class": null,
"position": {
"x": 950,
"y": 300,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"source": "stirrer",
"target": "reactor",
"type": "physical",
"port": {
"stirrer": "top",
"reactor": "bottom"
}
},
{
"source": "pump_add",
"target": "flask_sample",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_sample": "top"
}
},
{
"source": "pump_add",
"target": "flask_buffer1",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_buffer1": "top"
}
},
{
"source": "pump_add",
"target": "flask_buffer2",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_buffer2": "top"
}
},
{
"source": "pump_add",
"target": "reactor",
"type": "physical",
"port": {
"pump_add": "outlet",
"reactor": "top"
}
},
{
"source": "pump_add",
"target": "flask_waste",
"type": "physical",
"port": {
"pump_add": "outlet",
"flask_waste": "top"
}
},
{
"source": "pump_add",
"target": "flask_air",
"type": "physical",
"port": {
"pump_add": "inlet",
"flask_air": "top"
}
}
]
}

View File

@@ -0,0 +1,249 @@
{
"nodes": [
{
"id": "TransferTestStation",
"name": "液体转移测试工作站",
"children": [
"transfer_pump",
"flask_source1",
"flask_source2",
"flask_target1",
"flask_target2",
"reactor",
"flask_waste",
"flask_rinsing"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["TransferProtocol"]
},
"data": {}
},
{
"id": "transfer_pump",
"name": "转移泵",
"children": [],
"parent": "TransferTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 50.0,
"transfer_rate": 5.0
},
"data": {
"status": "Idle"
}
},
{
"id": "flask_source1",
"name": "源容器1",
"children": [],
"parent": "TransferTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_source2",
"name": "源容器2",
"children": [],
"parent": "TransferTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_target1",
"name": "目标容器1",
"children": [],
"parent": "TransferTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_target2",
"name": "目标容器2",
"children": [],
"parent": "TransferTestStation",
"type": "container",
"class": null,
"position": {
"x": 550,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "TransferTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_waste",
"name": "废液瓶",
"children": [],
"parent": "TransferTestStation",
"type": "container",
"class": null,
"position": {
"x": 850,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_rinsing",
"name": "冲洗液瓶",
"children": [],
"parent": "TransferTestStation",
"type": "container",
"class": null,
"position": {
"x": 950,
"y": 300,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"source": "transfer_pump",
"target": "flask_source1",
"type": "physical",
"port": {
"transfer_pump": "inlet",
"flask_source1": "top"
}
},
{
"source": "transfer_pump",
"target": "flask_source2",
"type": "physical",
"port": {
"transfer_pump": "inlet",
"flask_source2": "top"
}
},
{
"source": "transfer_pump",
"target": "flask_target1",
"type": "physical",
"port": {
"transfer_pump": "outlet",
"flask_target1": "top"
}
},
{
"source": "transfer_pump",
"target": "flask_target2",
"type": "physical",
"port": {
"transfer_pump": "outlet",
"flask_target2": "top"
}
},
{
"source": "transfer_pump",
"target": "reactor",
"type": "physical",
"port": {
"transfer_pump": "outlet",
"reactor": "top"
}
},
{
"source": "transfer_pump",
"target": "flask_waste",
"type": "physical",
"port": {
"transfer_pump": "outlet",
"flask_waste": "top"
}
},
{
"source": "transfer_pump",
"target": "flask_rinsing",
"type": "physical",
"port": {
"transfer_pump": "inlet",
"flask_rinsing": "top"
}
}
]
}

View File

@@ -0,0 +1,494 @@
{
"nodes": [
{
"id": "WashSolidTestStation",
"name": "固体清洗测试工作站",
"children": [
"transfer_pump_1",
"heatchill_1",
"stirrer_1",
"filter_1",
"flask_ethanol",
"flask_water",
"flask_acetone",
"flask_methanol",
"reactor",
"collection_flask",
"waste_flask",
"flask_sample",
"filtrate_flask"
],
"parent": null,
"type": "device",
"class": "workstation",
"position": {
"x": 620.6111111111111,
"y": 171,
"z": 0
},
"config": {
"protocol_type": ["WashSolidProtocol", "TransferProtocol", "FilterProtocol", "HeatChillProtocol", "StirProtocol"]
},
"data": {}
},
{
"id": "transfer_pump_1",
"name": "转移泵",
"children": [],
"parent": "WashSolidTestStation",
"type": "device",
"class": "virtual_transfer_pump",
"position": {
"x": 520.6111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_volume": 50.0,
"transfer_rate": 10.0
},
"data": {
"status": "Idle",
"current_volume": 0.0,
"max_volume": 50.0,
"transfer_rate": 10.0,
"from_vessel": "",
"to_vessel": "",
"progress": 0.0,
"transferred_volume": 0.0,
"current_status": "Ready"
}
},
{
"id": "heatchill_1",
"name": "加热冷却器",
"children": [],
"parent": "WashSolidTestStation",
"type": "device",
"class": "virtual_heatchill",
"position": {
"x": 650.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 150.0,
"min_temp": -20.0
},
"data": {
"status": "Idle",
"current_temp": 25.0,
"target_temp": 25.0,
"vessel": "",
"purpose": "",
"progress": 0.0,
"current_status": "Ready"
}
},
{
"id": "stirrer_1",
"name": "搅拌器",
"children": [],
"parent": "WashSolidTestStation",
"type": "device",
"class": "virtual_stirrer",
"position": {
"x": 750.1111111111111,
"y": 300,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_speed": 1000.0
},
"data": {
"status": "Idle"
}
},
{
"id": "filter_1",
"name": "过滤器",
"children": [],
"parent": "WashSolidTestStation",
"type": "device",
"class": "virtual_filter",
"position": {
"x": 850.1111111111111,
"y": 478,
"z": 0
},
"config": {
"port": "VIRTUAL",
"max_temp": 100.0,
"max_stir_speed": 1000.0
},
"data": {
"status": "Idle",
"filter_state": "Ready",
"current_temp": 25.0,
"target_temp": 25.0,
"max_temp": 100.0,
"stir_speed": 0.0,
"max_stir_speed": 1000.0,
"filtered_volume": 0.0,
"progress": 0.0,
"message": ""
}
},
{
"id": "flask_ethanol",
"name": "乙醇清洗剂",
"children": [],
"parent": "WashSolidTestStation",
"type": "container",
"class": null,
"position": {
"x": 100,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "ethanol",
"volume": 1500.0,
"concentration": 99.5
}
]
}
},
{
"id": "flask_water",
"name": "水清洗剂",
"children": [],
"parent": "WashSolidTestStation",
"type": "container",
"class": null,
"position": {
"x": 250,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": [
{
"name": "water",
"volume": 1800.0,
"concentration": 100.0
}
]
}
},
{
"id": "flask_acetone",
"name": "丙酮清洗剂",
"children": [],
"parent": "WashSolidTestStation",
"type": "container",
"class": null,
"position": {
"x": 400,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "acetone",
"volume": 800.0,
"concentration": 99.8
}
]
}
},
{
"id": "flask_methanol",
"name": "甲醇清洗剂",
"children": [],
"parent": "WashSolidTestStation",
"type": "container",
"class": null,
"position": {
"x": 550,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "methanol",
"volume": 800.0,
"concentration": 99.9
}
]
}
},
{
"id": "reactor",
"name": "反应器",
"children": [],
"parent": "WashSolidTestStation",
"type": "container",
"class": null,
"position": {
"x": 698.1111111111111,
"y": 428,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": [
{
"name": "solid_product",
"volume": 50.0,
"concentration": 100.0
}
]
}
},
{
"id": "collection_flask",
"name": "收集瓶",
"children": [],
"parent": "WashSolidTestStation",
"type": "container",
"class": null,
"position": {
"x": 850,
"y": 300,
"z": 0
},
"config": {
"max_volume": 1000.0
},
"data": {
"liquid": []
}
},
{
"id": "waste_flask",
"name": "废液瓶",
"children": [],
"parent": "WashSolidTestStation",
"type": "container",
"class": null,
"position": {
"x": 1000,
"y": 428,
"z": 0
},
"config": {
"max_volume": 2000.0
},
"data": {
"liquid": []
}
},
{
"id": "flask_sample",
"name": "样品瓶",
"children": [],
"parent": "WashSolidTestStation",
"type": "container",
"class": null,
"position": {
"x": 1150,
"y": 300,
"z": 0
},
"config": {
"max_volume": 500.0
},
"data": {
"liquid": [
{
"name": "crude_solid",
"volume": 30.0,
"concentration": 80.0
}
]
}
},
{
"id": "filtrate_flask",
"name": "滤液收集瓶",
"children": [],
"parent": "WashSolidTestStation",
"type": "container",
"class": null,
"position": {
"x": 1000,
"y": 300,
"z": 0
},
"config": {
"max_volume": 1500.0
},
"data": {
"liquid": []
}
}
],
"links": [
{
"source": "transfer_pump_1",
"target": "flask_ethanol",
"type": "physical",
"port": {
"transfer_pump_1": "1",
"flask_ethanol": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_water",
"type": "physical",
"port": {
"transfer_pump_1": "2",
"flask_water": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_acetone",
"type": "physical",
"port": {
"transfer_pump_1": "3",
"flask_acetone": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_methanol",
"type": "physical",
"port": {
"transfer_pump_1": "4",
"flask_methanol": "top"
}
},
{
"source": "transfer_pump_1",
"target": "reactor",
"type": "physical",
"port": {
"transfer_pump_1": "5",
"reactor": "top"
}
},
{
"source": "transfer_pump_1",
"target": "collection_flask",
"type": "physical",
"port": {
"transfer_pump_1": "6",
"collection_flask": "top"
}
},
{
"source": "transfer_pump_1",
"target": "waste_flask",
"type": "physical",
"port": {
"transfer_pump_1": "7",
"waste_flask": "top"
}
},
{
"source": "transfer_pump_1",
"target": "flask_sample",
"type": "physical",
"port": {
"transfer_pump_1": "8",
"flask_sample": "top"
}
},
{
"source": "transfer_pump_1",
"target": "filtrate_flask",
"type": "physical",
"port": {
"transfer_pump_1": "9",
"filtrate_flask": "top"
}
},
{
"source": "heatchill_1",
"target": "reactor",
"type": "physical",
"port": {
"heatchill_1": "heating_element",
"reactor": "bottom"
}
},
{
"source": "heatchill_1",
"target": "flask_sample",
"type": "physical",
"port": {
"heatchill_1": "heating_element",
"flask_sample": "bottom"
}
},
{
"source": "stirrer_1",
"target": "reactor",
"type": "physical",
"port": {
"stirrer_1": "stir_rod",
"reactor": "center"
}
},
{
"source": "stirrer_1",
"target": "flask_sample",
"type": "physical",
"port": {
"stirrer_1": "stir_rod",
"flask_sample": "center"
}
},
{
"source": "filter_1",
"target": "reactor",
"type": "physical",
"port": {
"filter_1": "filter_element",
"reactor": "top"
}
},
{
"source": "filter_1",
"target": "flask_sample",
"type": "physical",
"port": {
"filter_1": "filter_element",
"flask_sample": "top"
}
},
{
"source": "filter_1",
"target": "filtrate_flask",
"type": "physical",
"port": {
"filter_1": "filter_element",
"filtrate_flask": "top"
}
}
]
}

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ from copy import deepcopy
import yaml 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__)) 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 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(): def parse_args():
"""解析命令行参数""" """解析命令行参数"""
parser = argparse.ArgumentParser(description="Start Uni-Lab Edge server.") parser = argparse.ArgumentParser(description="Start Uni-Lab Edge server.")
@@ -58,6 +73,11 @@ def parse_args():
action="store_true", action="store_true",
help="Slave模式下跳过等待host服务", help="Slave模式下跳过等待host服务",
) )
parser.add_argument(
"--upload_registry",
action="store_true",
help="启动unilab时同时报送注册表信息",
)
parser.add_argument( parser.add_argument(
"--config", "--config",
type=str, type=str,
@@ -97,22 +117,12 @@ def main():
# 加载配置文件优先加载config然后从env读取 # 加载配置文件优先加载config然后从env读取
config_path = args_dict.get("config") config_path = args_dict.get("config")
if config_path is None: load_config_from_file(config_path)
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)
# 设置BasicConfig参数 # 设置BasicConfig参数
BasicConfig.is_host_mode = not args_dict.get("without_host", False) BasicConfig.is_host_mode = not args_dict.get("without_host", False)
BasicConfig.slave_no_host = args_dict.get("slave_no_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 = os.popen("hostname").read().strip()
machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name]) machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name])
BasicConfig.machine_name = machine_name BasicConfig.machine_name = machine_name
@@ -136,15 +146,16 @@ def main():
# 注册表 # 注册表
build_registry(args_dict["registry_path"]) build_registry(args_dict["registry_path"])
resource_edge_info = []
devices_and_resources = None devices_and_resources = None
if args_dict["graph"] is not None: if args_dict["graph"] is not None:
import unilabos.resources.graphio as graph_res import unilabos.resources.graphio as graph_res
graph_res.physical_setup_graph = ( if args_dict["graph"].endswith(".json"):
read_node_link_json(args_dict["graph"]) graph, data = read_node_link_json(args_dict["graph"])
if args_dict["graph"].endswith(".json") else:
else read_graphml(args_dict["graph"]) 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) 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"] = initialize_resources(list(deepcopy(devices_and_resources).values()))
args_dict["resources_config"] = list(devices_and_resources.values()) args_dict["resources_config"] = list(devices_and_resources.values())
@@ -185,6 +196,7 @@ def main():
signal.signal(signal.SIGTERM, _exit) signal.signal(signal.SIGTERM, _exit)
mqtt_client.start() mqtt_client.start()
args_dict["resources_mesh_config"] = {} args_dict["resources_mesh_config"] = {}
args_dict["resources_edge_config"] = resource_edge_info
# web visiualize 2D # web visiualize 2D
if args_dict["visual"] != "disable": if args_dict["visual"] != "disable":
enable_rviz = args_dict["visual"] == "rviz" 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} 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) 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: if self.mqtt_disable:
return return
address = f"labs/{MQConfig.lab_id}/registry/" address = f"labs/{MQConfig.lab_id}/registry/"
registry_data = json.dumps({device_id: device_info}, ensure_ascii=False, cls=TypeEncoder) registry_data = json.dumps({device_id: device_info}, ensure_ascii=False, cls=TypeEncoder)
self.client.publish(address, registry_data, qos=2) 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): def publish_actions(self, action_id: str, action_info: dict):
if self.mqtt_disable: 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 self.auth = MQConfig.lab_id
info(f"HTTPClient 初始化完成: remote_addr={self.remote_addr}") 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}", f"{self.remote_addr}/lab/resource/?database_process_later={1 if database_process_later else 0}",
json=resources, json=resources,
headers={"Authorization": f"lab {self.auth}"}, headers={"Authorization": f"lab {self.auth}"},
timeout=5, timeout=100,
) )
if response.status_code != 200:
logger.error(f"添加物料失败: {response.text}")
return response return response
def resource_get(self, id: str, with_children: bool = False) -> Dict[str, Any]: 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", f"{self.remote_addr}/lab/resource/?edge_format=1",
params={"id": id, "with_children": with_children}, params={"id": id, "with_children": with_children},
headers={"Authorization": f"lab {self.auth}"}, headers={"Authorization": f"lab {self.auth}"},
timeout=5, timeout=20,
) )
return response.json() return response.json()
@@ -81,7 +103,7 @@ class HTTPClient:
f"{self.remote_addr}/lab/resource/batch_delete/", f"{self.remote_addr}/lab/resource/batch_delete/",
params={"id": id}, params={"id": id},
headers={"Authorization": f"lab {self.auth}"}, headers={"Authorization": f"lab {self.auth}"},
timeout=5, timeout=20,
) )
return response return response
@@ -99,7 +121,7 @@ class HTTPClient:
f"{self.remote_addr}/lab/resource/batch_update/?edge_format=1", f"{self.remote_addr}/lab/resource/batch_update/?edge_format=1",
json=resources, json=resources,
headers={"Authorization": f"lab {self.auth}"}, headers={"Authorization": f"lab {self.auth}"},
timeout=5, timeout=100,
) )
return response return response

View File

@@ -16,7 +16,6 @@ from jinja2 import Environment, FileSystemLoader
from unilabos.config.config import BasicConfig from unilabos.config.config import BasicConfig
from unilabos.registry.registry import lab_registry 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.ros.msgs.message_converter import msg_converter_manager
from unilabos.utils.log import error from unilabos.utils.log import error
from unilabos.utils.type_check import TypeEncoder from unilabos.utils.type_check import TypeEncoder

View File

@@ -5,15 +5,45 @@ from .separate_protocol import generate_separate_protocol
from .evaporate_protocol import generate_evaporate_protocol from .evaporate_protocol import generate_evaporate_protocol
from .evacuateandrefill_protocol import generate_evacuateandrefill_protocol from .evacuateandrefill_protocol import generate_evacuateandrefill_protocol
from .agv_transfer_protocol import generate_agv_transfer_protocol from .agv_transfer_protocol import generate_agv_transfer_protocol
from .add_protocol import generate_add_protocol
from .centrifuge_protocol import generate_centrifuge_protocol
from .filter_protocol import generate_filter_protocol
from .heatchill_protocol import (
generate_heat_chill_protocol,
generate_heat_chill_start_protocol,
generate_heat_chill_stop_protocol,
generate_heat_chill_to_temp_protocol # 保留导入,但不注册为协议
)
from .stir_protocol import generate_stir_protocol, generate_start_stir_protocol, generate_stop_stir_protocol
from .transfer_protocol import generate_transfer_protocol
from .clean_vessel_protocol import generate_clean_vessel_protocol
from .dissolve_protocol import generate_dissolve_protocol
from .filter_through_protocol import generate_filter_through_protocol
from .run_column_protocol import generate_run_column_protocol
from .wash_solid_protocol import generate_wash_solid_protocol
# Define a dictionary of protocol generators. # Define a dictionary of protocol generators.
action_protocol_generators = { action_protocol_generators = {
PumpTransferProtocol: generate_pump_protocol_with_rinsing, AddProtocol: generate_add_protocol,
CleanProtocol: generate_clean_protocol,
SeparateProtocol: generate_separate_protocol,
EvaporateProtocol: generate_evaporate_protocol,
EvacuateAndRefillProtocol: generate_evacuateandrefill_protocol,
AGVTransferProtocol: generate_agv_transfer_protocol, AGVTransferProtocol: generate_agv_transfer_protocol,
} CentrifugeProtocol: generate_centrifuge_protocol,
# End Protocols CleanProtocol: generate_clean_protocol,
CleanVesselProtocol: generate_clean_vessel_protocol,
DissolveProtocol: generate_dissolve_protocol,
EvacuateAndRefillProtocol: generate_evacuateandrefill_protocol,
EvaporateProtocol: generate_evaporate_protocol,
FilterProtocol: generate_filter_protocol,
FilterThroughProtocol: generate_filter_through_protocol,
HeatChillProtocol: generate_heat_chill_protocol,
HeatChillStartProtocol: generate_heat_chill_start_protocol,
HeatChillStopProtocol: generate_heat_chill_stop_protocol,
PumpTransferProtocol: generate_pump_protocol_with_rinsing,
RunColumnProtocol: generate_run_column_protocol,
SeparateProtocol: generate_separate_protocol,
StartStirProtocol: generate_start_stir_protocol,
StirProtocol: generate_stir_protocol,
StopStirProtocol: generate_stop_stir_protocol,
TransferProtocol: generate_transfer_protocol,
WashSolidProtocol: generate_wash_solid_protocol,
}

View File

@@ -0,0 +1,627 @@
import networkx as nx
from typing import List, Dict, Any
from .pump_protocol import generate_pump_protocol_with_rinsing
def find_reagent_vessel(G: nx.DiGraph, reagent: str) -> str:
"""
根据试剂名称查找对应的试剂瓶,支持多种匹配模式:
1. 容器名称匹配(如 flask_DMF, reagent_bottle_1-DMF
2. 容器内液体类型匹配(如 liquid_type: "DMF", name: "ethanol"
3. 试剂名称匹配(如 reagent_name: "DMF", config.reagent: "ethyl_acetate"
Args:
G: 网络图
reagent: 试剂名称
Returns:
str: 试剂瓶的vessel ID
Raises:
ValueError: 如果找不到对应的试剂瓶
"""
print(f"ADD_PROTOCOL: 正在查找试剂 '{reagent}' 的容器...")
# 第一步:通过容器名称匹配
possible_names = [
f"flask_{reagent}", # flask_DMF, flask_ethanol
f"bottle_{reagent}", # bottle_DMF, bottle_ethanol
f"vessel_{reagent}", # vessel_DMF, vessel_ethanol
f"{reagent}_flask", # DMF_flask, ethanol_flask
f"{reagent}_bottle", # DMF_bottle, ethanol_bottle
f"{reagent}", # 直接用试剂名
f"reagent_{reagent}", # reagent_DMF, reagent_ethanol
f"reagent_bottle_{reagent}", # reagent_bottle_DMF
]
# 尝试名称匹配
for vessel_name in possible_names:
if vessel_name in G.nodes():
print(f"ADD_PROTOCOL: 通过名称匹配找到容器: {vessel_name}")
return vessel_name
# 第二步:通过模糊名称匹配(名称中包含试剂名)
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
# 检查节点ID或名称中是否包含试剂名
node_name = G.nodes[node_id].get('name', '').lower()
if (reagent.lower() in node_id.lower() or
reagent.lower() in node_name):
print(f"ADD_PROTOCOL: 通过模糊名称匹配找到容器: {node_id} (名称: {node_name})")
return node_id
# 第三步:通过液体类型匹配
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
vessel_data = G.nodes[node_id].get('data', {})
liquids = vessel_data.get('liquid', [])
for liquid in liquids:
if isinstance(liquid, dict):
# 支持两种格式的液体类型字段
liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
reagent_name = vessel_data.get('reagent_name', '')
config_reagent = G.nodes[node_id].get('config', {}).get('reagent', '')
# 检查多个可能的字段
if (liquid_type.lower() == reagent.lower() or
reagent_name.lower() == reagent.lower() or
config_reagent.lower() == reagent.lower()):
print(f"ADD_PROTOCOL: 通过液体类型匹配找到容器: {node_id}")
print(f" - liquid_type: {liquid_type}")
print(f" - reagent_name: {reagent_name}")
print(f" - config.reagent: {config_reagent}")
return node_id
# 第四步:列出所有可用的容器信息帮助调试
available_containers = []
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
vessel_data = G.nodes[node_id].get('data', {})
config_data = G.nodes[node_id].get('config', {})
liquids = vessel_data.get('liquid', [])
container_info = {
'id': node_id,
'name': G.nodes[node_id].get('name', ''),
'liquid_types': [],
'reagent_name': vessel_data.get('reagent_name', ''),
'config_reagent': config_data.get('reagent', '')
}
for liquid in liquids:
if isinstance(liquid, dict):
liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
if liquid_type:
container_info['liquid_types'].append(liquid_type)
available_containers.append(container_info)
print(f"ADD_PROTOCOL: 可用容器列表:")
for container in available_containers:
print(f" - {container['id']}: {container['name']}")
print(f" 液体类型: {container['liquid_types']}")
print(f" 试剂名称: {container['reagent_name']}")
print(f" 配置试剂: {container['config_reagent']}")
raise ValueError(f"找不到试剂 '{reagent}' 对应的试剂瓶。尝试了名称匹配: {possible_names}")
def find_reagent_vessel_by_any_match(G: nx.DiGraph, reagent: str) -> str:
"""
增强版试剂容器查找,支持各种匹配方式的别名函数
"""
return find_reagent_vessel(G, reagent)
def get_vessel_reagent_volume(G: nx.DiGraph, vessel: str) -> float:
"""获取容器中的试剂体积"""
if vessel not in G.nodes():
return 0.0
vessel_data = G.nodes[vessel].get('data', {})
liquids = vessel_data.get('liquid', [])
total_volume = 0.0
for liquid in liquids:
if isinstance(liquid, dict):
# 支持两种格式:新格式 (name, volume) 和旧格式 (liquid_type, liquid_volume)
volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0)
total_volume += volume
return total_volume
def get_vessel_reagent_types(G: nx.DiGraph, vessel: str) -> List[str]:
"""获取容器中所有试剂的类型"""
if vessel not in G.nodes():
return []
vessel_data = G.nodes[vessel].get('data', {})
liquids = vessel_data.get('liquid', [])
reagent_types = []
for liquid in liquids:
if isinstance(liquid, dict):
# 支持两种格式的试剂类型字段
reagent_type = liquid.get('liquid_type') or liquid.get('name', '')
if reagent_type:
reagent_types.append(reagent_type)
# 同时检查配置中的试剂信息
config_reagent = G.nodes[vessel].get('config', {}).get('reagent', '')
reagent_name = vessel_data.get('reagent_name', '')
if config_reagent and config_reagent not in reagent_types:
reagent_types.append(config_reagent)
if reagent_name and reagent_name not in reagent_types:
reagent_types.append(reagent_name)
return reagent_types
def find_vessels_by_reagent(G: nx.DiGraph, reagent: str) -> List[str]:
"""
根据试剂类型查找所有匹配的容器
返回匹配容器的ID列表
"""
matching_vessels = []
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
# 检查容器名称匹配
node_name = G.nodes[node_id].get('name', '').lower()
if reagent.lower() in node_id.lower() or reagent.lower() in node_name:
matching_vessels.append(node_id)
continue
# 检查试剂类型匹配
vessel_data = G.nodes[node_id].get('data', {})
liquids = vessel_data.get('liquid', [])
config_data = G.nodes[node_id].get('config', {})
# 检查 reagent_name 和 config.reagent
reagent_name = vessel_data.get('reagent_name', '').lower()
config_reagent = config_data.get('reagent', '').lower()
if (reagent.lower() == reagent_name or
reagent.lower() == config_reagent):
matching_vessels.append(node_id)
continue
# 检查液体列表
for liquid in liquids:
if isinstance(liquid, dict):
liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
if liquid_type.lower() == reagent.lower():
matching_vessels.append(node_id)
break
return matching_vessels
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
"""
查找与指定容器相连的搅拌器
Args:
G: 网络图
vessel: 容器ID
Returns:
str: 搅拌器ID如果找不到则返回None
"""
# 查找所有搅拌器节点
stirrer_nodes = [node for node in G.nodes()
if (G.nodes[node].get('class') or '') == 'virtual_stirrer']
# 检查哪个搅拌器与目标容器相连
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
return stirrer
# 如果没有直接连接,返回第一个可用的搅拌器
return stirrer_nodes[0] if stirrer_nodes else None
def generate_add_protocol(
G: nx.DiGraph,
vessel: str,
reagent: str,
volume: float,
mass: float = 0.0,
amount: str = "",
time: float = 0.0,
stir: bool = False,
stir_speed: float = 300.0,
viscous: bool = False,
purpose: str = "添加试剂"
) -> List[Dict[str, Any]]:
"""
生成添加试剂的协议序列,支持智能试剂匹配
基于pump_protocol的成熟算法实现试剂添加功能
1. 智能查找试剂瓶(支持名称匹配、液体类型匹配、试剂配置匹配)
2. **先启动搅拌,再进行转移** - 确保试剂添加更均匀
3. 使用pump_protocol实现液体转移
Args:
G: 有向图,节点为容器和设备,边为连接关系
vessel: 目标容器(要添加试剂的容器)
reagent: 试剂名称(用于查找对应的试剂瓶)
volume: 要添加的体积 (mL)
mass: 要添加的质量 (g) - 暂时未使用,预留接口
amount: 其他数量描述
time: 添加时间 (s),如果指定则计算流速
stir: 是否启用搅拌
stir_speed: 搅拌速度 (RPM)
viscous: 是否为粘稠液体
purpose: 添加目的描述
Returns:
List[Dict[str, Any]]: 动作序列
Raises:
ValueError: 当找不到必要的设备或容器时
"""
action_sequence = []
print(f"ADD_PROTOCOL: 开始生成添加试剂协议")
print(f" - 目标容器: {vessel}")
print(f" - 试剂: {reagent}")
print(f" - 体积: {volume} mL")
print(f" - 质量: {mass} g")
print(f" - 搅拌: {stir} (速度: {stir_speed} RPM)")
print(f" - 粘稠: {viscous}")
print(f" - 目的: {purpose}")
# 1. 验证目标容器存在
if vessel not in G.nodes():
raise ValueError(f"目标容器 '{vessel}' 不存在于系统中")
# 2. 智能查找试剂瓶
try:
reagent_vessel = find_reagent_vessel(G, reagent)
print(f"ADD_PROTOCOL: 找到试剂容器: {reagent_vessel}")
except ValueError as e:
raise ValueError(f"无法找到试剂 '{reagent}': {str(e)}")
# 3. 验证试剂容器中的试剂体积
available_volume = get_vessel_reagent_volume(G, reagent_vessel)
print(f"ADD_PROTOCOL: 试剂容器 {reagent_vessel} 中有 {available_volume} mL 试剂")
if available_volume < volume:
print(f"ADD_PROTOCOL: 警告 - 试剂容器中的试剂不足!需要 {volume} mL可用 {available_volume} mL")
# 4. 验证是否存在从试剂瓶到目标容器的路径
try:
path = nx.shortest_path(G, source=reagent_vessel, target=vessel)
print(f"ADD_PROTOCOL: 找到路径 {reagent_vessel} -> {vessel}: {path}")
except nx.NetworkXNoPath:
raise ValueError(f"从试剂瓶 '{reagent_vessel}' 到目标容器 '{vessel}' 没有可用路径")
# 5. **先启动搅拌** - 关键改进!
if stir:
try:
stirrer_id = find_connected_stirrer(G, vessel)
if stirrer_id:
print(f"ADD_PROTOCOL: 找到搅拌器 {stirrer_id},将在添加前启动搅拌")
# 先启动搅拌
stir_action = {
"device_id": stirrer_id,
"action_name": "start_stir",
"action_kwargs": {
"vessel": vessel,
"stir_speed": stir_speed,
"purpose": f"{purpose}: 启动搅拌,准备添加 {reagent}"
}
}
action_sequence.append(stir_action)
print(f"ADD_PROTOCOL: 已添加搅拌动作,速度 {stir_speed} RPM")
# 等待搅拌稳定
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
})
else:
print(f"ADD_PROTOCOL: 警告 - 需要搅拌但未找到与容器 {vessel} 相连的搅拌器")
except Exception as e:
print(f"ADD_PROTOCOL: 搅拌器配置出错: {str(e)}")
# 6. 如果指定了体积,执行液体转移
if volume > 0:
# 6.1 计算流速参数
if time > 0:
# 根据时间计算流速
transfer_flowrate = volume / time
flowrate = transfer_flowrate
else:
# 使用默认流速
if viscous:
transfer_flowrate = 0.3 # 粘稠液体用较慢速度
flowrate = 1.0
else:
transfer_flowrate = 0.5 # 普通液体默认速度
flowrate = 2.5
print(f"ADD_PROTOCOL: 准备转移 {volume} mL 从 {reagent_vessel}{vessel}")
print(f"ADD_PROTOCOL: 转移流速={transfer_flowrate} mL/s, 注入流速={flowrate} mL/s")
# 6.2 使用pump_protocol的核心算法实现液体转移
try:
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
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,
flowrate=flowrate,
transfer_flowrate=transfer_flowrate
)
# 添加pump actions到序列中
action_sequence.extend(pump_actions)
except Exception as e:
raise ValueError(f"生成泵协议时出错: {str(e)}")
print(f"ADD_PROTOCOL: 生成了 {len(action_sequence)} 个动作")
print(f"ADD_PROTOCOL: 添加试剂协议生成完成")
return action_sequence
def generate_add_protocol_with_cleaning(
G: nx.DiGraph,
vessel: str,
reagent: str,
volume: float,
mass: float = 0.0,
amount: str = "",
time: float = 0.0,
stir: bool = False,
stir_speed: float = 300.0,
viscous: bool = False,
purpose: str = "添加试剂",
cleaning_solvent: str = "air",
cleaning_volume: float = 5.0,
cleaning_repeats: int = 1
) -> List[Dict[str, Any]]:
"""
生成带清洗的添加试剂协议,支持智能试剂匹配
与普通添加协议的区别是会在添加后进行管道清洗
Args:
G: 有向图
vessel: 目标容器
reagent: 试剂名称
volume: 添加体积
mass: 添加质量(预留)
amount: 其他数量描述
time: 添加时间
stir: 是否搅拌
stir_speed: 搅拌速度
viscous: 是否粘稠
purpose: 添加目的
cleaning_solvent: 清洗溶剂("air"表示空气清洗)
cleaning_volume: 清洗体积
cleaning_repeats: 清洗重复次数
Returns:
List[Dict[str, Any]]: 动作序列
"""
action_sequence = []
# 1. 智能查找试剂瓶
reagent_vessel = find_reagent_vessel(G, reagent)
# 2. **先启动搅拌**
if stir:
stirrer_id = find_connected_stirrer(G, vessel)
if stirrer_id:
action_sequence.append({
"device_id": stirrer_id,
"action_name": "start_stir",
"action_kwargs": {
"vessel": vessel,
"stir_speed": stir_speed,
"purpose": f"{purpose}: 启动搅拌,准备添加 {reagent}"
}
})
# 等待搅拌稳定
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
})
# 3. 计算流速
if time > 0:
transfer_flowrate = volume / time
flowrate = transfer_flowrate
else:
if viscous:
transfer_flowrate = 0.3
flowrate = 1.0
else:
transfer_flowrate = 0.5
flowrate = 2.5
# 4. 使用带清洗的pump_protocol
pump_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=reagent_vessel,
to_vessel=vessel,
volume=volume,
amount=amount,
time=time,
viscous=viscous,
rinsing_solvent=cleaning_solvent,
rinsing_volume=cleaning_volume,
rinsing_repeats=cleaning_repeats,
solid=False,
flowrate=flowrate,
transfer_flowrate=transfer_flowrate
)
action_sequence.extend(pump_actions)
return action_sequence
def generate_sequential_add_protocol(
G: nx.DiGraph,
vessel: str,
reagents: List[Dict[str, Any]],
stir_between_additions: bool = True,
final_stir: bool = True,
final_stir_speed: float = 400.0,
final_stir_time: float = 300.0
) -> List[Dict[str, Any]]:
"""
生成连续添加多种试剂的协议,支持智能试剂匹配
Args:
G: 网络图
vessel: 目标容器
reagents: 试剂列表,每个元素包含试剂添加参数
stir_between_additions: 是否在每次添加之间搅拌
final_stir: 是否在所有添加完成后进行最终搅拌
final_stir_speed: 最终搅拌速度
final_stir_time: 最终搅拌时间
Returns:
List[Dict[str, Any]]: 完整的动作序列
Example:
reagents = [
{
"reagent": "DMF", # 会匹配 reagent_bottle_1 (reagent_name: "DMF")
"volume": 10.0,
"viscous": False,
"stir_speed": 300.0
},
{
"reagent": "ethyl_acetate", # 会匹配 reagent_bottle_2 (reagent_name: "ethyl_acetate")
"volume": 5.0,
"viscous": False,
"stir_speed": 350.0
}
]
"""
action_sequence = []
print(f"ADD_PROTOCOL: 开始连续添加 {len(reagents)} 种试剂到容器 {vessel}")
for i, reagent_params in enumerate(reagents):
reagent_name = reagent_params.get('reagent')
print(f"ADD_PROTOCOL: 处理第 {i+1}/{len(reagents)} 个试剂: {reagent_name}")
# 生成单个试剂的添加协议
add_actions = generate_add_protocol(
G=G,
vessel=vessel,
reagent=reagent_name,
volume=reagent_params.get('volume', 0.0),
mass=reagent_params.get('mass', 0.0),
amount=reagent_params.get('amount', ''),
time=reagent_params.get('time', 0.0),
stir=stir_between_additions,
stir_speed=reagent_params.get('stir_speed', 300.0),
viscous=reagent_params.get('viscous', False),
purpose=reagent_params.get('purpose', f'添加试剂 {reagent_name} ({i+1}/{len(reagents)})')
)
action_sequence.extend(add_actions)
# 在添加之间加入等待时间
if i < len(reagents) - 1: # 不是最后一个试剂
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 10} # 试剂混合时间
})
# 最终搅拌
if final_stir:
stirrer_id = find_connected_stirrer(G, vessel)
if stirrer_id:
print(f"ADD_PROTOCOL: 添加最终搅拌动作,速度 {final_stir_speed} RPM时间 {final_stir_time}")
action_sequence.extend([
{
"device_id": stirrer_id,
"action_name": "stir",
"action_kwargs": {
"stir_time": final_stir_time,
"stir_speed": final_stir_speed,
"settling_time": 30.0
}
}
])
print(f"ADD_PROTOCOL: 连续添加协议生成完成,共 {len(action_sequence)} 个动作")
return action_sequence
# 便捷函数:常用添加方案
def generate_organic_add_protocol(
G: nx.DiGraph,
vessel: str,
organic_reagent: str,
volume: float,
stir_speed: float = 400.0
) -> List[Dict[str, Any]]:
"""有机试剂添加:慢速、搅拌"""
return generate_add_protocol(
G, vessel, organic_reagent, volume, 0.0, "", 0.0,
True, stir_speed, False, f"添加有机试剂 {organic_reagent}"
)
def generate_viscous_add_protocol(
G: nx.DiGraph,
vessel: str,
viscous_reagent: str,
volume: float,
addition_time: float = 120.0
) -> List[Dict[str, Any]]:
"""粘稠试剂添加:慢速、长时间"""
return generate_add_protocol(
G, vessel, viscous_reagent, volume, 0.0, "", addition_time,
True, 250.0, True, f"缓慢添加粘稠试剂 {viscous_reagent}"
)
def generate_solvent_add_protocol(
G: nx.DiGraph,
vessel: str,
solvent: str,
volume: float
) -> List[Dict[str, Any]]:
"""溶剂添加:快速、无需特殊处理"""
return generate_add_protocol(
G, vessel, solvent, volume, 0.0, "", 0.0,
False, 300.0, False, f"添加溶剂 {solvent}"
)
# 使用示例和测试函数
def test_add_protocol():
"""测试添加协议的示例"""
print("=== ADD PROTOCOL 智能匹配测试 ===")
print("测试完成")
if __name__ == "__main__":
test_add_protocol()

View File

@@ -0,0 +1,285 @@
from typing import List, Dict, Any
import networkx as nx
from .pump_protocol import generate_pump_protocol
def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
"""
获取容器中的液体体积
"""
if vessel not in G.nodes():
return 0.0
vessel_data = G.nodes[vessel].get('data', {})
liquids = vessel_data.get('liquid', [])
total_volume = 0.0
for liquid in liquids:
if isinstance(liquid, dict) and 'liquid_volume' in liquid:
total_volume += liquid['liquid_volume']
return total_volume
def find_centrifuge_device(G: nx.DiGraph) -> str:
"""
查找离心机设备
"""
centrifuge_nodes = [node for node in G.nodes()
if (G.nodes[node].get('class') or '') == 'virtual_centrifuge']
if centrifuge_nodes:
return centrifuge_nodes[0]
raise ValueError("系统中未找到离心机设备")
def find_centrifuge_vessel(G: nx.DiGraph) -> str:
"""
查找离心机专用容器
"""
possible_names = [
"centrifuge_tube",
"centrifuge_vessel",
"tube_centrifuge",
"vessel_centrifuge",
"centrifuge",
"tube_15ml",
"tube_50ml"
]
for vessel_name in possible_names:
if vessel_name in G.nodes():
return vessel_name
raise ValueError(f"未找到离心机容器。尝试了以下名称: {possible_names}")
def generate_centrifuge_protocol(
G: nx.DiGraph,
vessel: str,
speed: float,
time: float,
temp: float = 25.0
) -> List[Dict[str, Any]]:
"""
生成离心操作的协议序列,复用 pump_protocol 的成熟算法
离心流程:
1. 液体转移:将待离心溶液从源容器转移到离心机容器
2. 离心操作:执行离心分离
3. 上清液转移:将离心后的上清液转移回原容器或新容器
4. 沉淀处理:处理离心沉淀(可选)
Args:
G: 有向图,节点为设备和容器,边为流体管道
vessel: 包含待离心溶液的容器名称
speed: 离心速度 (rpm)
time: 离心时间 (秒)
temp: 离心温度 (°C)默认25°C
Returns:
List[Dict[str, Any]]: 离心操作的动作序列
Raises:
ValueError: 当找不到必要的设备时抛出异常
Examples:
centrifuge_actions = generate_centrifuge_protocol(G, "reaction_mixture", 5000, 600, 4.0)
"""
action_sequence = []
print(f"CENTRIFUGE: 开始生成离心协议")
print(f" - 源容器: {vessel}")
print(f" - 离心速度: {speed} rpm")
print(f" - 离心时间: {time}s ({time/60:.1f}分钟)")
print(f" - 离心温度: {temp}°C")
# 验证源容器存在
if vessel not in G.nodes():
raise ValueError(f"源容器 '{vessel}' 不存在于系统中")
# 获取源容器中的液体体积
source_volume = get_vessel_liquid_volume(G, vessel)
print(f"CENTRIFUGE: 源容器 {vessel} 中有 {source_volume} mL 液体")
# 查找离心机设备
try:
centrifuge_id = find_centrifuge_device(G)
print(f"CENTRIFUGE: 找到离心机: {centrifuge_id}")
except ValueError as e:
raise ValueError(f"无法找到离心机: {str(e)}")
# 查找离心机容器
try:
centrifuge_vessel = find_centrifuge_vessel(G)
print(f"CENTRIFUGE: 找到离心机容器: {centrifuge_vessel}")
except ValueError as e:
raise ValueError(f"无法找到离心机容器: {str(e)}")
# === 简化的体积计算策略 ===
if source_volume > 0:
# 如果能检测到液体体积,使用实际体积的大部分
transfer_volume = min(source_volume * 0.9, 15.0) # 90%或最多15mL离心管通常较小
print(f"CENTRIFUGE: 检测到液体体积,将转移 {transfer_volume} mL")
else:
# 如果检测不到液体体积,默认转移标准量
transfer_volume = 10.0 # 标准离心管体积
print(f"CENTRIFUGE: 未检测到液体体积,默认转移 {transfer_volume} mL")
# === 第一步:将待离心溶液转移到离心机容器 ===
print(f"CENTRIFUGE: 将 {transfer_volume} mL 溶液从 {vessel} 转移到 {centrifuge_vessel}")
try:
# 使用成熟的 pump_protocol 算法进行液体转移
transfer_to_centrifuge_actions = generate_pump_protocol(
G=G,
from_vessel=vessel,
to_vessel=centrifuge_vessel,
volume=transfer_volume,
flowrate=1.0, # 离心转移用慢速,避免气泡
transfer_flowrate=1.0
)
action_sequence.extend(transfer_to_centrifuge_actions)
except Exception as e:
raise ValueError(f"无法将溶液转移到离心机: {str(e)}")
# 转移后等待
wait_action = {
"action_name": "wait",
"action_kwargs": {"time": 5}
}
action_sequence.append(wait_action)
# === 第二步:执行离心操作 ===
print(f"CENTRIFUGE: 执行离心操作")
centrifuge_action = {
"device_id": centrifuge_id,
"action_name": "centrifuge",
"action_kwargs": {
"vessel": centrifuge_vessel,
"speed": speed,
"time": time,
"temp": temp
}
}
action_sequence.append(centrifuge_action)
# 离心后等待系统稳定
wait_action = {
"action_name": "wait",
"action_kwargs": {"time": 10} # 离心后等待稍长,让沉淀稳定
}
action_sequence.append(wait_action)
# === 第三步:将上清液转移回原容器 ===
print(f"CENTRIFUGE: 将上清液从离心机转移回 {vessel}")
try:
# 估算上清液体积约为转移体积的80% - 假设20%成为沉淀)
supernatant_volume = transfer_volume * 0.8
print(f"CENTRIFUGE: 预计上清液体积 {supernatant_volume} mL")
transfer_back_actions = generate_pump_protocol(
G=G,
from_vessel=centrifuge_vessel,
to_vessel=vessel,
volume=supernatant_volume,
flowrate=0.5, # 上清液转移更慢,避免扰动沉淀
transfer_flowrate=0.5
)
action_sequence.extend(transfer_back_actions)
except Exception as e:
print(f"CENTRIFUGE: 将上清液转移回容器失败: {str(e)}")
# === 第四步:清洗离心机容器 ===
print(f"CENTRIFUGE: 清洗离心机容器")
try:
# 查找清洗溶剂
cleaning_solvent = None
for solvent in ["flask_water", "flask_ethanol", "flask_acetone"]:
if solvent in G.nodes():
cleaning_solvent = solvent
break
if cleaning_solvent:
# 用少量溶剂清洗离心管
cleaning_volume = 5.0 # 5mL清洗
print(f"CENTRIFUGE: 用 {cleaning_volume} mL {cleaning_solvent} 清洗")
# 清洗溶剂加入
cleaning_actions = generate_pump_protocol(
G=G,
from_vessel=cleaning_solvent,
to_vessel=centrifuge_vessel,
volume=cleaning_volume,
flowrate=2.0,
transfer_flowrate=2.0
)
action_sequence.extend(cleaning_actions)
# 将清洗液转移到废液
if "waste_workup" in G.nodes():
waste_actions = generate_pump_protocol(
G=G,
from_vessel=centrifuge_vessel,
to_vessel="waste_workup",
volume=cleaning_volume,
flowrate=2.0,
transfer_flowrate=2.0
)
action_sequence.extend(waste_actions)
except Exception as e:
print(f"CENTRIFUGE: 清洗步骤失败: {str(e)}")
print(f"CENTRIFUGE: 生成了 {len(action_sequence)} 个动作")
print(f"CENTRIFUGE: 离心协议生成完成")
print(f"CENTRIFUGE: 总处理体积: {transfer_volume} mL")
return action_sequence
# 便捷函数:常用离心方案
def generate_low_speed_centrifuge_protocol(
G: nx.DiGraph,
vessel: str,
time: float = 300.0 # 5分钟
) -> List[Dict[str, Any]]:
"""低速离心:细胞分离或大颗粒沉淀"""
return generate_centrifuge_protocol(G, vessel, 1000.0, time, 4.0)
def generate_high_speed_centrifuge_protocol(
G: nx.DiGraph,
vessel: str,
time: float = 600.0 # 10分钟
) -> List[Dict[str, Any]]:
"""高速离心:蛋白质沉淀或小颗粒分离"""
return generate_centrifuge_protocol(G, vessel, 12000.0, time, 4.0)
def generate_standard_centrifuge_protocol(
G: nx.DiGraph,
vessel: str,
time: float = 600.0 # 10分钟
) -> List[Dict[str, Any]]:
"""标准离心:常规样品处理"""
return generate_centrifuge_protocol(G, vessel, 5000.0, time, 25.0)
def generate_cold_centrifuge_protocol(
G: nx.DiGraph,
vessel: str,
speed: float = 5000.0,
time: float = 600.0
) -> List[Dict[str, Any]]:
"""冷冻离心:热敏感样品处理"""
return generate_centrifuge_protocol(G, vessel, speed, time, 4.0)
def generate_ultra_centrifuge_protocol(
G: nx.DiGraph,
vessel: str,
time: float = 1800.0 # 30分钟
) -> List[Dict[str, Any]]:
"""超高速离心:超细颗粒分离"""
return generate_centrifuge_protocol(G, vessel, 15000.0, time, 4.0)

View File

@@ -0,0 +1,439 @@
from typing import List, Dict, Any
import networkx as nx
from .pump_protocol import generate_pump_protocol
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
"""
查找溶剂容器,支持多种匹配模式:
1. 容器名称匹配(如 flask_water, reagent_bottle_1-DMF
2. 容器内液体类型匹配(如 liquid_type: "DMF", "ethanol"
"""
print(f"CLEAN_VESSEL: 正在查找溶剂 '{solvent}' 的容器...")
# 第一步:通过容器名称匹配
possible_names = [
f"flask_{solvent}", # flask_water, flask_ethanol
f"bottle_{solvent}", # bottle_water, bottle_ethanol
f"vessel_{solvent}", # vessel_water, vessel_ethanol
f"{solvent}_flask", # water_flask, ethanol_flask
f"{solvent}_bottle", # water_bottle, ethanol_bottle
f"{solvent}", # 直接用溶剂名
f"solvent_{solvent}", # solvent_water, solvent_ethanol
f"reagent_bottle_{solvent}", # reagent_bottle_DMF
]
# 尝试名称匹配
for vessel_name in possible_names:
if vessel_name in G.nodes():
print(f"CLEAN_VESSEL: 通过名称匹配找到容器: {vessel_name}")
return vessel_name
# 第二步:通过模糊名称匹配(名称中包含溶剂名)
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
# 检查节点ID或名称中是否包含溶剂名
node_name = G.nodes[node_id].get('name', '').lower()
if (solvent.lower() in node_id.lower() or
solvent.lower() in node_name):
print(f"CLEAN_VESSEL: 通过模糊名称匹配找到容器: {node_id} (名称: {node_name})")
return node_id
# 第三步:通过液体类型匹配
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
vessel_data = G.nodes[node_id].get('data', {})
liquids = vessel_data.get('liquid', [])
for liquid in liquids:
if isinstance(liquid, dict):
# 支持两种格式的液体类型字段
liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
reagent_name = vessel_data.get('reagent_name', '')
config_reagent = G.nodes[node_id].get('config', {}).get('reagent', '')
# 检查多个可能的字段
if (liquid_type.lower() == solvent.lower() or
reagent_name.lower() == solvent.lower() or
config_reagent.lower() == solvent.lower()):
print(f"CLEAN_VESSEL: 通过液体类型匹配找到容器: {node_id}")
print(f" - liquid_type: {liquid_type}")
print(f" - reagent_name: {reagent_name}")
print(f" - config.reagent: {config_reagent}")
return node_id
# 第四步:列出所有可用的容器信息帮助调试
available_containers = []
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
vessel_data = G.nodes[node_id].get('data', {})
config_data = G.nodes[node_id].get('config', {})
liquids = vessel_data.get('liquid', [])
container_info = {
'id': node_id,
'name': G.nodes[node_id].get('name', ''),
'liquid_types': [],
'reagent_name': vessel_data.get('reagent_name', ''),
'config_reagent': config_data.get('reagent', '')
}
for liquid in liquids:
if isinstance(liquid, dict):
liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
if liquid_type:
container_info['liquid_types'].append(liquid_type)
available_containers.append(container_info)
print(f"CLEAN_VESSEL: 可用容器列表:")
for container in available_containers:
print(f" - {container['id']}: {container['name']}")
print(f" 液体类型: {container['liquid_types']}")
print(f" 试剂名称: {container['reagent_name']}")
print(f" 配置试剂: {container['config_reagent']}")
raise ValueError(f"未找到溶剂 '{solvent}' 的容器。尝试了名称匹配: {possible_names}")
def find_solvent_vessel_by_any_match(G: nx.DiGraph, solvent: str) -> str:
"""
增强版溶剂容器查找,支持各种匹配方式的别名函数
"""
return find_solvent_vessel(G, solvent)
def find_waste_vessel(G: nx.DiGraph) -> str:
"""
查找废液容器
"""
possible_waste_names = [
"waste_workup",
"flask_waste",
"bottle_waste",
"waste",
"waste_vessel",
"waste_container"
]
for waste_name in possible_waste_names:
if waste_name in G.nodes():
return waste_name
raise ValueError(f"未找到废液容器。尝试了以下名称: {possible_waste_names}")
def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str:
"""
查找与指定容器相连的加热冷却设备
"""
# 查找所有加热冷却设备节点
heatchill_nodes = [node for node in G.nodes()
if (G.nodes[node].get('class') or '') == 'virtual_heatchill']
# 检查哪个加热设备与目标容器相连(机械连接)
for heatchill in heatchill_nodes:
if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill):
return heatchill
# 如果没有直接连接,返回第一个可用的加热设备
if heatchill_nodes:
return heatchill_nodes[0]
return None # 没有加热设备也可以工作,只是不能加热
def generate_clean_vessel_protocol(
G: nx.DiGraph,
vessel: str,
solvent: str,
volume: float,
temp: float,
repeats: int = 1
) -> List[Dict[str, Any]]:
"""
生成容器清洗操作的协议序列,复用 pump_protocol 的成熟算法
清洗流程:
1. 查找溶剂容器和废液容器
2. 如果需要加热,启动加热设备
3. 重复以下操作 repeats 次:
a. 使用 pump_protocol 将溶剂从溶剂容器转移到目标容器
b. (可选) 等待清洗作用时间
c. 使用 pump_protocol 将清洗液从目标容器转移到废液容器
4. 如果加热了,停止加热
Args:
G: 有向图,节点为设备和容器,边为流体管道
vessel: 要清洗的容器名称
solvent: 用于清洗的溶剂名称
volume: 每次清洗使用的溶剂体积
temp: 清洗时的温度
repeats: 清洗操作的重复次数,默认为 1
Returns:
List[Dict[str, Any]]: 容器清洗操作的动作序列
Raises:
ValueError: 当找不到必要的容器或设备时抛出异常
Examples:
clean_protocol = generate_clean_vessel_protocol(G, "main_reactor", "water", 100.0, 60.0, 2)
"""
action_sequence = []
print(f"CLEAN_VESSEL: 开始生成容器清洗协议")
print(f" - 目标容器: {vessel}")
print(f" - 清洗溶剂: {solvent}")
print(f" - 清洗体积: {volume} mL")
print(f" - 清洗温度: {temp}°C")
print(f" - 重复次数: {repeats}")
# 验证目标容器存在
if vessel not in G.nodes():
raise ValueError(f"目标容器 '{vessel}' 不存在于系统中")
# 查找溶剂容器
try:
solvent_vessel = find_solvent_vessel(G, solvent)
print(f"CLEAN_VESSEL: 找到溶剂容器: {solvent_vessel}")
except ValueError as e:
raise ValueError(f"无法找到溶剂容器: {str(e)}")
# 查找废液容器
try:
waste_vessel = find_waste_vessel(G)
print(f"CLEAN_VESSEL: 找到废液容器: {waste_vessel}")
except ValueError as e:
raise ValueError(f"无法找到废液容器: {str(e)}")
# 查找加热设备(可选)
heatchill_id = find_connected_heatchill(G, vessel)
if heatchill_id:
print(f"CLEAN_VESSEL: 找到加热设备: {heatchill_id}")
else:
print(f"CLEAN_VESSEL: 未找到加热设备,将在室温下清洗")
# 第一步:如果需要加热且有加热设备,启动加热
if temp > 25.0 and heatchill_id:
print(f"CLEAN_VESSEL: 启动加热至 {temp}°C")
heatchill_start_action = {
"device_id": heatchill_id,
"action_name": "heat_chill_start",
"action_kwargs": {
"vessel": vessel,
"temp": temp,
"purpose": f"cleaning with {solvent}"
}
}
action_sequence.append(heatchill_start_action)
# 等待温度稳定
wait_action = {
"action_name": "wait",
"action_kwargs": {"time": 30} # 等待30秒让温度稳定
}
action_sequence.append(wait_action)
# 第二步:重复清洗操作
for repeat in range(repeats):
print(f"CLEAN_VESSEL: 执行第 {repeat + 1} 次清洗")
# 2a. 使用 pump_protocol 将溶剂转移到目标容器
print(f"CLEAN_VESSEL: 将 {volume} mL {solvent} 转移到 {vessel}")
try:
# 调用成熟的 pump_protocol 算法
add_solvent_actions = generate_pump_protocol(
G=G,
from_vessel=solvent_vessel,
to_vessel=vessel,
volume=volume,
flowrate=2.5, # 适中的流速,避免飞溅
transfer_flowrate=2.5
)
action_sequence.extend(add_solvent_actions)
except Exception as e:
raise ValueError(f"无法将溶剂转移到容器: {str(e)}")
# 2b. 等待清洗作用时间(让溶剂充分清洗容器)
cleaning_wait_time = 60 if temp > 50.0 else 30 # 高温下等待更久
print(f"CLEAN_VESSEL: 等待清洗作用 {cleaning_wait_time}")
wait_action = {
"action_name": "wait",
"action_kwargs": {"time": cleaning_wait_time}
}
action_sequence.append(wait_action)
# 2c. 使用 pump_protocol 将清洗液转移到废液容器
print(f"CLEAN_VESSEL: 将清洗液从 {vessel} 转移到废液容器")
try:
# 调用成熟的 pump_protocol 算法
remove_waste_actions = generate_pump_protocol(
G=G,
from_vessel=vessel,
to_vessel=waste_vessel,
volume=volume,
flowrate=2.5, # 适中的流速
transfer_flowrate=2.5
)
action_sequence.extend(remove_waste_actions)
except Exception as e:
raise ValueError(f"无法将清洗液转移到废液容器: {str(e)}")
# 2d. 清洗循环间的短暂等待
if repeat < repeats - 1: # 不是最后一次清洗
print(f"CLEAN_VESSEL: 清洗循环间等待")
wait_action = {
"action_name": "wait",
"action_kwargs": {"time": 10}
}
action_sequence.append(wait_action)
# 第三步:如果加热了,停止加热
if temp > 25.0 and heatchill_id:
print(f"CLEAN_VESSEL: 停止加热")
heatchill_stop_action = {
"device_id": heatchill_id,
"action_name": "heat_chill_stop",
"action_kwargs": {
"vessel": vessel
}
}
action_sequence.append(heatchill_stop_action)
print(f"CLEAN_VESSEL: 生成了 {len(action_sequence)} 个动作")
print(f"CLEAN_VESSEL: 清洗协议生成完成")
return action_sequence
# 便捷函数:常用清洗方案
def generate_quick_clean_protocol(
G: nx.DiGraph,
vessel: str,
solvent: str = "water",
volume: float = 100.0
) -> List[Dict[str, Any]]:
"""快速清洗:室温,单次清洗"""
return generate_clean_vessel_protocol(G, vessel, solvent, volume, 25.0, 1)
def generate_thorough_clean_protocol(
G: nx.DiGraph,
vessel: str,
solvent: str = "water",
volume: float = 150.0,
temp: float = 60.0
) -> List[Dict[str, Any]]:
"""深度清洗:加热,多次清洗"""
return generate_clean_vessel_protocol(G, vessel, solvent, volume, temp, 3)
def generate_organic_clean_protocol(
G: nx.DiGraph,
vessel: str,
volume: float = 100.0
) -> List[Dict[str, Any]]:
"""有机清洗:先用有机溶剂,再用水清洗"""
action_sequence = []
# 第一步:有机溶剂清洗
try:
organic_actions = generate_clean_vessel_protocol(
G, vessel, "acetone", volume, 25.0, 2
)
action_sequence.extend(organic_actions)
except ValueError:
# 如果没有丙酮,尝试乙醇
try:
organic_actions = generate_clean_vessel_protocol(
G, vessel, "ethanol", volume, 25.0, 2
)
action_sequence.extend(organic_actions)
except ValueError:
print("警告:未找到有机溶剂,跳过有机清洗步骤")
# 第二步:水清洗
water_actions = generate_clean_vessel_protocol(
G, vessel, "water", volume, 25.0, 2
)
action_sequence.extend(water_actions)
return action_sequence
def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
"""获取容器中的液体体积(修复版)"""
if vessel not in G.nodes():
return 0.0
vessel_data = G.nodes[vessel].get('data', {})
liquids = vessel_data.get('liquid', [])
total_volume = 0.0
for liquid in liquids:
if isinstance(liquid, dict):
# 支持两种格式:新格式 (name, volume) 和旧格式 (liquid_type, liquid_volume)
volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0)
total_volume += volume
return total_volume
def get_vessel_liquid_types(G: nx.DiGraph, vessel: str) -> List[str]:
"""获取容器中所有液体的类型"""
if vessel not in G.nodes():
return []
vessel_data = G.nodes[vessel].get('data', {})
liquids = vessel_data.get('liquid', [])
liquid_types = []
for liquid in liquids:
if isinstance(liquid, dict):
# 支持两种格式的液体类型字段
liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
if liquid_type:
liquid_types.append(liquid_type)
return liquid_types
def find_vessel_by_content(G: nx.DiGraph, content: str) -> List[str]:
"""
根据内容物查找所有匹配的容器
返回匹配容器的ID列表
"""
matching_vessels = []
for node_id in G.nodes():
if G.nodes[node_id].get('type') == 'container':
# 检查容器名称匹配
node_name = G.nodes[node_id].get('name', '').lower()
if content.lower() in node_id.lower() or content.lower() in node_name:
matching_vessels.append(node_id)
continue
# 检查液体类型匹配
vessel_data = G.nodes[node_id].get('data', {})
liquids = vessel_data.get('liquid', [])
config_data = G.nodes[node_id].get('config', {})
# 检查 reagent_name 和 config.reagent
reagent_name = vessel_data.get('reagent_name', '').lower()
config_reagent = config_data.get('reagent', '').lower()
if (content.lower() == reagent_name or
content.lower() == config_reagent):
matching_vessels.append(node_id)
continue
# 检查液体列表
for liquid in liquids:
if isinstance(liquid, dict):
liquid_type = liquid.get('liquid_type') or liquid.get('name', '')
if liquid_type.lower() == content.lower():
matching_vessels.append(node_id)
break
return matching_vessels

View File

@@ -0,0 +1,359 @@
from typing import List, Dict, Any
import networkx as nx
from .pump_protocol import generate_pump_protocol
def find_solvent_vessel(G: nx.DiGraph, solvent: str) -> str:
"""
查找溶剂容器
"""
# 按照pump_protocol的命名规则查找溶剂瓶
solvent_vessel_id = f"flask_{solvent}"
if solvent_vessel_id in G.nodes():
return solvent_vessel_id
# 如果直接匹配失败,尝试模糊匹配
for node in G.nodes():
if node.startswith('flask_') and solvent.lower() in node.lower():
return node
# 如果还是找不到,列出所有可用的溶剂瓶
available_flasks = [node for node in G.nodes()
if node.startswith('flask_')
and G.nodes[node].get('type') == 'container']
raise ValueError(f"找不到溶剂 '{solvent}' 对应的溶剂瓶。可用溶剂瓶: {available_flasks}")
def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str:
"""
查找与指定容器相连的加热搅拌器
"""
# 查找所有加热搅拌器节点
heatchill_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_heatchill']
# 检查哪个加热器与目标容器相连
for heatchill in heatchill_nodes:
if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill):
return heatchill
# 如果没有直接连接,返回第一个可用的加热器
return heatchill_nodes[0] if heatchill_nodes else None
def generate_dissolve_protocol(
G: nx.DiGraph,
vessel: str,
solvent: str,
volume: float,
amount: str = "",
temp: float = 25.0,
time: float = 0.0,
stir_speed: float = 300.0
) -> List[Dict[str, Any]]:
"""
生成溶解操作的协议序列,复用 pump_protocol 的成熟算法
溶解流程:
1. 溶剂转移:将溶剂从溶剂瓶转移到目标容器
2. 启动加热搅拌:设置温度和搅拌
3. 等待溶解:监控溶解过程
4. 停止加热搅拌:完成溶解
Args:
G: 有向图,节点为设备和容器,边为流体管道
vessel: 目标容器(要进行溶解的容器)
solvent: 溶剂名称(用于查找对应的溶剂瓶)
volume: 溶剂体积 (mL)
amount: 要溶解的物质描述
temp: 溶解温度 (°C)默认25°C室温
time: 溶解时间 (秒)默认0立即完成
stir_speed: 搅拌速度 (RPM)默认300 RPM
Returns:
List[Dict[str, Any]]: 溶解操作的动作序列
Raises:
ValueError: 当找不到必要的设备或容器时
Examples:
dissolve_actions = generate_dissolve_protocol(G, "reaction_mixture", "DMF", 10.0, "NaCl 2g", 60.0, 600.0, 400.0)
"""
action_sequence = []
print(f"DISSOLVE: 开始生成溶解协议")
print(f" - 目标容器: {vessel}")
print(f" - 溶剂: {solvent}")
print(f" - 溶剂体积: {volume} mL")
print(f" - 要溶解的物质: {amount}")
print(f" - 溶解温度: {temp}°C")
print(f" - 溶解时间: {time}s ({time/60:.1f}分钟)" if time > 0 else " - 溶解时间: 立即完成")
print(f" - 搅拌速度: {stir_speed} RPM")
# 验证目标容器存在
if vessel not in G.nodes():
raise ValueError(f"目标容器 '{vessel}' 不存在于系统中")
# 查找溶剂瓶
try:
solvent_vessel = find_solvent_vessel(G, solvent)
print(f"DISSOLVE: 找到溶剂瓶: {solvent_vessel}")
except ValueError as e:
raise ValueError(f"无法找到溶剂 '{solvent}': {str(e)}")
# 验证是否存在从溶剂瓶到目标容器的路径
try:
path = nx.shortest_path(G, source=solvent_vessel, target=vessel)
print(f"DISSOLVE: 找到路径 {solvent_vessel} -> {vessel}: {path}")
except nx.NetworkXNoPath:
raise ValueError(f"从溶剂瓶 '{solvent_vessel}' 到目标容器 '{vessel}' 没有可用路径")
# 查找加热搅拌器
heatchill_id = None
if temp > 25.0 or stir_speed > 0 or time > 0:
try:
heatchill_id = find_connected_heatchill(G, vessel)
if heatchill_id:
print(f"DISSOLVE: 找到加热搅拌器: {heatchill_id}")
else:
print(f"DISSOLVE: 警告 - 需要加热/搅拌但未找到与容器 {vessel} 相连的加热搅拌器")
except Exception as e:
print(f"DISSOLVE: 加热搅拌器配置出错: {str(e)}")
# === 第一步:启动加热搅拌(在添加溶剂前) ===
if heatchill_id and (temp > 25.0 or time > 0):
print(f"DISSOLVE: 启动加热搅拌器,温度: {temp}°C")
if time > 0:
# 如果指定了时间,使用定时加热搅拌
heatchill_action = {
"device_id": heatchill_id,
"action_name": "heat_chill",
"action_kwargs": {
"vessel": vessel,
"temp": temp,
"time": time,
"stir": True,
"stir_speed": stir_speed,
"purpose": f"溶解 {amount}{solvent}"
}
}
else:
# 如果没有指定时间,使用持续加热搅拌
heatchill_action = {
"device_id": heatchill_id,
"action_name": "heat_chill_start",
"action_kwargs": {
"vessel": vessel,
"temp": temp,
"purpose": f"溶解 {amount}{solvent}"
}
}
action_sequence.append(heatchill_action)
# 等待温度稳定
if temp > 25.0:
wait_time = min(60, abs(temp - 25.0) * 1.5) # 根据温差估算预热时间
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": wait_time}
})
# === 第二步:添加溶剂到目标容器 ===
if volume > 0:
print(f"DISSOLVE: 将 {volume} mL {solvent}{solvent_vessel} 转移到 {vessel}")
# 计算流速 - 溶解时通常用较慢的速度,避免飞溅
transfer_flowrate = 1.0 # 较慢的转移速度
flowrate = 0.5 # 较慢的注入速度
try:
# 使用成熟的 pump_protocol 算法进行液体转移
pump_actions = generate_pump_protocol(
G=G,
from_vessel=solvent_vessel,
to_vessel=vessel,
volume=volume,
flowrate=flowrate, # 注入速度 - 较慢避免飞溅
transfer_flowrate=transfer_flowrate # 转移速度
)
action_sequence.extend(pump_actions)
except Exception as e:
raise ValueError(f"生成泵协议时出错: {str(e)}")
# 溶剂添加后等待
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
})
# === 第三步:如果没有使用定时加热搅拌,但需要等待溶解 ===
if time > 0 and heatchill_id and temp <= 25.0:
# 只需要搅拌等待,不需要加热
print(f"DISSOLVE: 室温搅拌 {time}s 等待溶解")
stir_action = {
"device_id": heatchill_id,
"action_name": "heat_chill",
"action_kwargs": {
"vessel": vessel,
"temp": 25.0, # 室温
"time": time,
"stir": True,
"stir_speed": stir_speed,
"purpose": f"室温搅拌溶解 {amount}"
}
}
action_sequence.append(stir_action)
# === 第四步:如果使用了持续加热,需要手动停止 ===
if heatchill_id and time == 0 and temp > 25.0:
print(f"DISSOLVE: 停止加热搅拌器")
stop_action = {
"device_id": heatchill_id,
"action_name": "heat_chill_stop",
"action_kwargs": {
"vessel": vessel
}
}
action_sequence.append(stop_action)
print(f"DISSOLVE: 生成了 {len(action_sequence)} 个动作")
print(f"DISSOLVE: 溶解协议生成完成")
return action_sequence
# 便捷函数:常用溶解方案
def generate_room_temp_dissolve_protocol(
G: nx.DiGraph,
vessel: str,
solvent: str,
volume: float,
amount: str = "",
stir_time: float = 300.0 # 5分钟
) -> List[Dict[str, Any]]:
"""室温溶解:快速搅拌,短时间"""
return generate_dissolve_protocol(G, vessel, solvent, volume, amount, 25.0, stir_time, 400.0)
def generate_heated_dissolve_protocol(
G: nx.DiGraph,
vessel: str,
solvent: str,
volume: float,
amount: str = "",
temp: float = 60.0,
dissolve_time: float = 900.0 # 15分钟
) -> List[Dict[str, Any]]:
"""加热溶解:中等温度,较长时间"""
return generate_dissolve_protocol(G, vessel, solvent, volume, amount, temp, dissolve_time, 300.0)
def generate_gentle_dissolve_protocol(
G: nx.DiGraph,
vessel: str,
solvent: str,
volume: float,
amount: str = "",
temp: float = 40.0,
dissolve_time: float = 1800.0 # 30分钟
) -> List[Dict[str, Any]]:
"""温和溶解:低温,长时间,慢搅拌"""
return generate_dissolve_protocol(G, vessel, solvent, volume, amount, temp, dissolve_time, 200.0)
def generate_hot_dissolve_protocol(
G: nx.DiGraph,
vessel: str,
solvent: str,
volume: float,
amount: str = "",
temp: float = 80.0,
dissolve_time: float = 600.0 # 10分钟
) -> List[Dict[str, Any]]:
"""高温溶解:高温,中等时间,快搅拌"""
return generate_dissolve_protocol(G, vessel, solvent, volume, amount, temp, dissolve_time, 500.0)
def generate_sequential_dissolve_protocol(
G: nx.DiGraph,
vessel: str,
dissolve_steps: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""
生成连续溶解多种物质的协议
Args:
G: 网络图
vessel: 目标容器
dissolve_steps: 溶解步骤列表,每个元素包含溶解参数
Returns:
List[Dict[str, Any]]: 完整的动作序列
Example:
dissolve_steps = [
{
"solvent": "water",
"volume": 5.0,
"amount": "NaCl 1g",
"temp": 25.0,
"time": 300.0,
"stir_speed": 300.0
},
{
"solvent": "ethanol",
"volume": 2.0,
"amount": "organic compound 0.5g",
"temp": 40.0,
"time": 600.0,
"stir_speed": 400.0
}
]
"""
action_sequence = []
for i, step in enumerate(dissolve_steps):
print(f"DISSOLVE: 处理第 {i+1}/{len(dissolve_steps)} 个溶解步骤")
# 生成单个溶解步骤的协议
dissolve_actions = generate_dissolve_protocol(
G=G,
vessel=vessel,
solvent=step.get('solvent'),
volume=step.get('volume', 0.0),
amount=step.get('amount', ''),
temp=step.get('temp', 25.0),
time=step.get('time', 0.0),
stir_speed=step.get('stir_speed', 300.0)
)
action_sequence.extend(dissolve_actions)
# 在步骤之间加入等待时间
if i < len(dissolve_steps) - 1: # 不是最后一个步骤
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 10}
})
print(f"DISSOLVE: 连续溶解协议生成完成,共 {len(action_sequence)} 个动作")
return action_sequence
# 测试函数
def test_dissolve_protocol():
"""测试溶解协议的示例"""
print("=== DISSOLVE PROTOCOL 测试 ===")
print("测试完成")
if __name__ == "__main__":
test_dissolve_protocol()

View File

@@ -1,143 +1,437 @@
import numpy as np import numpy as np
import networkx as nx import networkx as nx
from typing import List, Dict, Any, Optional
from .pump_protocol import generate_pump_protocol_with_rinsing, generate_pump_protocol
def find_gas_source(G: nx.DiGraph, gas: str) -> str:
"""根据气体名称查找对应的气源"""
# 按照命名规则查找气源
gas_source_patterns = [
f"gas_source_{gas}",
f"gas_{gas}",
f"flask_{gas}",
f"{gas}_source"
]
for pattern in gas_source_patterns:
if pattern in G.nodes():
return pattern
# 模糊匹配
for node in G.nodes():
node_class = G.nodes[node].get('class', '') or ''
if 'gas_source' in node_class and gas.lower() in node.lower():
return node
if node.startswith('flask_') and gas.lower() in node.lower():
return node
# 查找所有可用的气源
available_gas_sources = [
node for node in G.nodes()
if ((G.nodes[node].get('class') or '').startswith('virtual_gas_source')
or ('gas' in node and 'source' in node)
or (node.startswith('flask_') and any(g in node.lower() for g in ['air', 'nitrogen', 'argon', 'vacuum'])))
]
raise ValueError(f"找不到气体 '{gas}' 对应的气源。可用气源: {available_gas_sources}")
def find_vacuum_pump(G: nx.DiGraph) -> str:
"""查找真空泵设备"""
vacuum_pumps = [
node for node in G.nodes()
if ((G.nodes[node].get('class') or '').startswith('virtual_vacuum_pump')
or 'vacuum_pump' in node
or 'vacuum' in (G.nodes[node].get('class') or ''))
]
if not vacuum_pumps:
raise ValueError("系统中未找到真空泵设备")
return vacuum_pumps[0]
def find_connected_stirrer(G: nx.DiGraph, vessel: str) -> str:
"""查找与指定容器相连的搅拌器"""
stirrer_nodes = [node for node in G.nodes()
if (G.nodes[node].get('class') or '') == 'virtual_stirrer']
# 检查哪个搅拌器与目标容器相连
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
return stirrer
return stirrer_nodes[0] if stirrer_nodes else None
def find_associated_solenoid_valve(G: nx.DiGraph, device_id: str) -> Optional[str]:
"""查找与指定设备相关联的电磁阀"""
solenoid_valves = [
node for node in G.nodes()
if ('solenoid' in (G.nodes[node].get('class') or '').lower()
or 'solenoid_valve' in node)
]
# 通过网络连接查找直接相连的电磁阀
for solenoid in solenoid_valves:
if G.has_edge(device_id, solenoid) or G.has_edge(solenoid, device_id):
return solenoid
# 通过命名规则查找关联的电磁阀
device_type = ""
if 'vacuum' in device_id.lower():
device_type = "vacuum"
elif 'gas' in device_id.lower():
device_type = "gas"
if device_type:
for solenoid in solenoid_valves:
if device_type in solenoid.lower():
return solenoid
return None
def generate_evacuateandrefill_protocol( def generate_evacuateandrefill_protocol(
G: nx.DiGraph, G: nx.DiGraph,
vessel: str, vessel: str,
gas: str, gas: str,
repeats: int = 1 repeats: int = 1
) -> list[dict]: ) -> List[Dict[str, Any]]:
""" """
生成操作的动作序列 生成抽真空和充气操作的动作序列
:param G: 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置 **修复版本**: 正确调用 pump_protocol 并处理异常
:param from_vessel: 容器A
:param to_vessel: 容器B
:param volume: 转移的体积
:param flowrate: 最终注入容器B时的流速
:param transfer_flowrate: 泵骨架中转移流速(若不指定,默认与注入流速相同)
:return: 泵操作的动作序列
""" """
action_sequence = []
# 生成电磁阀、真空泵、气源操作的动作序列 # 参数设置 - 关键修复:减小体积避免超出泵容量
vacuum_action_sequence = [] VACUUM_VOLUME = 20.0 # 减小抽真空体积
nodes = G.nodes(data=True) REFILL_VOLUME = 20.0 # 减小充气体积
PUMP_FLOW_RATE = 2.5 # 降低流速
STIR_SPEED = 300.0
# 找到和 vessel 相连的电磁阀和真空泵、气源 print(f"EVACUATE_REFILL: 开始生成协议,目标容器: {vessel}, 气体: {gas}, 重复次数: {repeats}")
vacuum_backbone = {"vessel": vessel}
for neighbor in G.neighbors(vessel): # 1. 验证设备存在
if nodes[neighbor]["class"].startswith("solenoid_valve"): if vessel not in G.nodes():
for neighbor2 in G.neighbors(neighbor): raise ValueError(f"目标容器 '{vessel}' 不存在于系统中")
if neighbor2 == vessel:
continue
if nodes[neighbor2]["class"].startswith("vacuum_pump"):
vacuum_backbone.update({"vacuum_valve": neighbor, "pump": neighbor2})
break
elif nodes[neighbor2]["class"].startswith("gas_source"):
vacuum_backbone.update({"gas_valve": neighbor, "gas": neighbor2})
break
# 判断是否设备齐全
if len(vacuum_backbone) < 5:
print(f"\n\n\n{vacuum_backbone}\n\n\n")
raise ValueError("Not all devices are connected to the vessel.")
# 生成操作的动作序列 # 2. 查找设备
for i in range(repeats): try:
# 打开真空泵阀门、关闭气源阀门 vacuum_pump = find_vacuum_pump(G)
vacuum_action_sequence.append([ vacuum_solenoid = find_associated_solenoid_valve(G, vacuum_pump)
{ gas_source = find_gas_source(G, gas)
"device_id": vacuum_backbone["vacuum_valve"], gas_solenoid = find_associated_solenoid_valve(G, gas_source)
"action_name": "set_valve_position", stirrer_id = find_connected_stirrer(G, vessel)
"action_kwargs": {
"command": "OPEN"
}
},
{
"device_id": vacuum_backbone["gas_valve"],
"action_name": "set_valve_position",
"action_kwargs": {
"command": "CLOSED"
}
}
])
# 打开真空泵、关闭气源 print(f"EVACUATE_REFILL: 找到设备")
vacuum_action_sequence.append([ print(f" - 真空泵: {vacuum_pump}")
{ print(f" - 气源: {gas_source}")
"device_id": vacuum_backbone["pump"], print(f" - 真空电磁阀: {vacuum_solenoid}")
"action_name": "set_status", print(f" - 气源电磁阀: {gas_solenoid}")
"action_kwargs": { print(f" - 搅拌器: {stirrer_id}")
"command": "ON"
}
},
{
"device_id": vacuum_backbone["gas"],
"action_name": "set_status",
"action_kwargs": {
"command": "OFF"
}
}
])
vacuum_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 60}})
# 关闭真空泵阀门、打开气源阀门 except ValueError as e:
vacuum_action_sequence.append([ raise ValueError(f"设备查找失败: {str(e)}")
{
"device_id": vacuum_backbone["vacuum_valve"], # 3. **关键修复**: 验证路径存在性
"action_name": "set_valve_position", try:
"action_kwargs": { # 验证抽真空路径
"command": "CLOSED" vacuum_path = nx.shortest_path(G, source=vessel, target=vacuum_pump)
} print(f"EVACUATE_REFILL: 抽真空路径: {''.join(vacuum_path)}")
},
{
"device_id": vacuum_backbone["gas_valve"],
"action_name": "set_valve_position",
"action_kwargs": {
"command": "OPEN"
}
}
])
# 关闭真空泵、打开气源 # 验证充气路径
vacuum_action_sequence.append([ gas_path = nx.shortest_path(G, source=gas_source, target=vessel)
{ print(f"EVACUATE_REFILL: 充气路径: {''.join(gas_path)}")
"device_id": vacuum_backbone["pump"],
"action_name": "set_status", # **新增**: 检查路径中的边数据
"action_kwargs": { for i in range(len(vacuum_path) - 1):
"command": "OFF" nodeA, nodeB = vacuum_path[i], vacuum_path[i + 1]
} edge_data = G.get_edge_data(nodeA, nodeB)
}, if not edge_data or 'port' not in edge_data:
{ raise ValueError(f"路径 {nodeA}{nodeB} 缺少端口信息")
"device_id": vacuum_backbone["gas"], print(f" 抽真空路径边 {nodeA}{nodeB}: {edge_data}")
"action_name": "set_status",
"action_kwargs": { for i in range(len(gas_path) - 1):
"command": "ON" nodeA, nodeB = gas_path[i], gas_path[i + 1]
} edge_data = G.get_edge_data(nodeA, nodeB)
if not edge_data or 'port' not in edge_data:
raise ValueError(f"路径 {nodeA}{nodeB} 缺少端口信息")
print(f" 充气路径边 {nodeA}{nodeB}: {edge_data}")
except nx.NetworkXNoPath as e:
raise ValueError(f"路径不存在: {str(e)}")
except Exception as e:
raise ValueError(f"路径验证失败: {str(e)}")
# 4. 启动搅拌器
if stirrer_id:
action_sequence.append({
"device_id": stirrer_id,
"action_name": "start_stir",
"action_kwargs": {
"vessel": vessel,
"stir_speed": STIR_SPEED,
"purpose": "抽真空充气操作前启动搅拌"
} }
]) })
vacuum_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 60}})
# 5. 执行多次抽真空-充气循环
for cycle in range(repeats):
print(f"EVACUATE_REFILL: === 第 {cycle+1}/{repeats} 次循环 ===")
# ============ 抽真空阶段 ============
print(f"EVACUATE_REFILL: 抽真空阶段开始")
# 启动真空泵
action_sequence.append({
"device_id": vacuum_pump,
"action_name": "set_status",
"action_kwargs": {"string": "ON"}
})
# 开启真空电磁阀
if vacuum_solenoid:
action_sequence.append({
"device_id": vacuum_solenoid,
"action_name": "set_valve_position",
"action_kwargs": {"command": "OPEN"}
})
# **关键修复**: 改进 pump_protocol 调用和错误处理
print(f"EVACUATE_REFILL: 调用抽真空 pump_protocol: {vessel}{vacuum_pump}")
print(f" - 体积: {VACUUM_VOLUME} mL")
print(f" - 流速: {PUMP_FLOW_RATE} mL/s")
try:
vacuum_transfer_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=vessel,
to_vessel=vacuum_pump,
volume=VACUUM_VOLUME,
amount="",
time=0.0,
viscous=False,
rinsing_solvent="", # **修复**: 明确不使用清洗
rinsing_volume=0.0,
rinsing_repeats=0,
solid=False,
flowrate=PUMP_FLOW_RATE,
transfer_flowrate=PUMP_FLOW_RATE
)
if vacuum_transfer_actions:
action_sequence.extend(vacuum_transfer_actions)
print(f"EVACUATE_REFILL: ✅ 成功添加 {len(vacuum_transfer_actions)} 个抽真空动作")
else:
print(f"EVACUATE_REFILL: ⚠️ 抽真空 pump_protocol 返回空序列")
# **修复**: 添加手动泵动作作为备选
action_sequence.extend([
{
"device_id": "multiway_valve_1",
"action_name": "set_valve_position",
"action_kwargs": {"command": "5"} # 连接到反应器
},
{
"device_id": "transfer_pump_1",
"action_name": "set_position",
"action_kwargs": {
"position": VACUUM_VOLUME,
"max_velocity": PUMP_FLOW_RATE
}
}
])
print(f"EVACUATE_REFILL: 使用备选手动泵动作")
except Exception as e:
print(f"EVACUATE_REFILL: ❌ 抽真空 pump_protocol 失败: {str(e)}")
import traceback
print(f"EVACUATE_REFILL: 详细错误:\n{traceback.format_exc()}")
# **修复**: 添加手动动作而不是忽略错误
print(f"EVACUATE_REFILL: 使用手动备选方案")
action_sequence.extend([
{
"device_id": "multiway_valve_1",
"action_name": "set_valve_position",
"action_kwargs": {"command": "5"} # 反应器端口
},
{
"device_id": "transfer_pump_1",
"action_name": "set_position",
"action_kwargs": {
"position": VACUUM_VOLUME,
"max_velocity": PUMP_FLOW_RATE
}
}
])
# 关闭真空电磁阀
if vacuum_solenoid:
action_sequence.append({
"device_id": vacuum_solenoid,
"action_name": "set_valve_position",
"action_kwargs": {"command": "CLOSED"}
})
# 关闭真空泵
action_sequence.append({
"device_id": vacuum_pump,
"action_name": "set_status",
"action_kwargs": {"string": "OFF"}
})
# ============ 充气阶段 ============
print(f"EVACUATE_REFILL: 充气阶段开始")
# 启动气源
action_sequence.append({
"device_id": gas_source,
"action_name": "set_status",
"action_kwargs": {"string": "ON"}
})
# 开启气源电磁阀
if gas_solenoid:
action_sequence.append({
"device_id": gas_solenoid,
"action_name": "set_valve_position",
"action_kwargs": {"command": "OPEN"}
})
# **关键修复**: 改进充气 pump_protocol 调用
print(f"EVACUATE_REFILL: 调用充气 pump_protocol: {gas_source}{vessel}")
try:
gas_transfer_actions = generate_pump_protocol_with_rinsing(
G=G,
from_vessel=gas_source,
to_vessel=vessel,
volume=REFILL_VOLUME,
amount="",
time=0.0,
viscous=False,
rinsing_solvent="", # **修复**: 明确不使用清洗
rinsing_volume=0.0,
rinsing_repeats=0,
solid=False,
flowrate=PUMP_FLOW_RATE,
transfer_flowrate=PUMP_FLOW_RATE
)
if gas_transfer_actions:
action_sequence.extend(gas_transfer_actions)
print(f"EVACUATE_REFILL: ✅ 成功添加 {len(gas_transfer_actions)} 个充气动作")
else:
print(f"EVACUATE_REFILL: ⚠️ 充气 pump_protocol 返回空序列")
# **修复**: 添加手动充气动作
action_sequence.extend([
{
"device_id": "multiway_valve_2",
"action_name": "set_valve_position",
"action_kwargs": {"command": "8"} # 氮气端口
},
{
"device_id": "transfer_pump_2",
"action_name": "set_position",
"action_kwargs": {
"position": REFILL_VOLUME,
"max_velocity": PUMP_FLOW_RATE
}
},
{
"device_id": "multiway_valve_2",
"action_name": "set_valve_position",
"action_kwargs": {"command": "5"} # 反应器端口
},
{
"device_id": "transfer_pump_2",
"action_name": "set_position",
"action_kwargs": {
"position": 0.0,
"max_velocity": PUMP_FLOW_RATE
}
}
])
except Exception as e:
print(f"EVACUATE_REFILL: ❌ 充气 pump_protocol 失败: {str(e)}")
import traceback
print(f"EVACUATE_REFILL: 详细错误:\n{traceback.format_exc()}")
# **修复**: 使用手动充气动作
print(f"EVACUATE_REFILL: 使用手动充气方案")
action_sequence.extend([
{
"device_id": "multiway_valve_2",
"action_name": "set_valve_position",
"action_kwargs": {"command": "8"} # 连接气源
},
{
"device_id": "transfer_pump_2",
"action_name": "set_position",
"action_kwargs": {
"position": REFILL_VOLUME,
"max_velocity": PUMP_FLOW_RATE
}
},
{
"device_id": "multiway_valve_2",
"action_name": "set_valve_position",
"action_kwargs": {"command": "5"} # 连接反应器
},
{
"device_id": "transfer_pump_2",
"action_name": "set_position",
"action_kwargs": {
"position": 0.0,
"max_velocity": PUMP_FLOW_RATE
}
}
])
# 关闭气源电磁阀
if gas_solenoid:
action_sequence.append({
"device_id": gas_solenoid,
"action_name": "set_valve_position",
"action_kwargs": {"command": "CLOSED"}
})
# 关闭气源 # 关闭气源
vacuum_action_sequence.append( action_sequence.append({
{ "device_id": gas_source,
"device_id": vacuum_backbone["gas"], "action_name": "set_status",
"action_name": "set_status", "action_kwargs": {"string": "OFF"}
"action_kwargs": { })
"command": "OFF"
}
}
)
# 关闭阀门 # 等待下一次循环
vacuum_action_sequence.append( if cycle < repeats - 1:
{ action_sequence.append({
"device_id": vacuum_backbone["gas_valve"], "action_name": "wait",
"action_name": "set_valve_position", "action_kwargs": {"time": 2.0}
"action_kwargs": { })
"command": "CLOSED"
} # 停止搅拌器
} if stirrer_id:
) action_sequence.append({
return vacuum_action_sequence "device_id": stirrer_id,
"action_name": "stop_stir",
"action_kwargs": {"vessel": vessel}
})
print(f"EVACUATE_REFILL: 协议生成完成,共 {len(action_sequence)} 个动作")
return action_sequence
# 测试函数
def test_evacuateandrefill_protocol():
"""测试抽真空充气协议"""
print("=== EVACUATE AND REFILL PROTOCOL 测试 ===")
print("测试完成")
if __name__ == "__main__":
test_evacuateandrefill_protocol()

View File

@@ -0,0 +1,143 @@
# import numpy as np
# import networkx as nx
# def generate_evacuateandrefill_protocol(
# G: nx.DiGraph,
# vessel: str,
# gas: str,
# repeats: int = 1
# ) -> list[dict]:
# """
# 生成泵操作的动作序列。
# :param G: 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置
# :param from_vessel: 容器A
# :param to_vessel: 容器B
# :param volume: 转移的体积
# :param flowrate: 最终注入容器B时的流速
# :param transfer_flowrate: 泵骨架中转移流速(若不指定,默认与注入流速相同)
# :return: 泵操作的动作序列
# """
# # 生成电磁阀、真空泵、气源操作的动作序列
# vacuum_action_sequence = []
# nodes = G.nodes(data=True)
# # 找到和 vessel 相连的电磁阀和真空泵、气源
# vacuum_backbone = {"vessel": vessel}
# for neighbor in G.neighbors(vessel):
# if nodes[neighbor]["class"].startswith("solenoid_valve"):
# for neighbor2 in G.neighbors(neighbor):
# if neighbor2 == vessel:
# continue
# if nodes[neighbor2]["class"].startswith("vacuum_pump"):
# vacuum_backbone.update({"vacuum_valve": neighbor, "pump": neighbor2})
# break
# elif nodes[neighbor2]["class"].startswith("gas_source"):
# vacuum_backbone.update({"gas_valve": neighbor, "gas": neighbor2})
# break
# # 判断是否设备齐全
# if len(vacuum_backbone) < 5:
# print(f"\n\n\n{vacuum_backbone}\n\n\n")
# raise ValueError("Not all devices are connected to the vessel.")
# # 生成操作的动作序列
# for i in range(repeats):
# # 打开真空泵阀门、关闭气源阀门
# vacuum_action_sequence.append([
# {
# "device_id": vacuum_backbone["vacuum_valve"],
# "action_name": "set_valve_position",
# "action_kwargs": {
# "command": "OPEN"
# }
# },
# {
# "device_id": vacuum_backbone["gas_valve"],
# "action_name": "set_valve_position",
# "action_kwargs": {
# "command": "CLOSED"
# }
# }
# ])
# # 打开真空泵、关闭气源
# vacuum_action_sequence.append([
# {
# "device_id": vacuum_backbone["pump"],
# "action_name": "set_status",
# "action_kwargs": {
# "string": "ON"
# }
# },
# {
# "device_id": vacuum_backbone["gas"],
# "action_name": "set_status",
# "action_kwargs": {
# "string": "OFF"
# }
# }
# ])
# vacuum_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 60}})
# # 关闭真空泵阀门、打开气源阀门
# vacuum_action_sequence.append([
# {
# "device_id": vacuum_backbone["vacuum_valve"],
# "action_name": "set_valve_position",
# "action_kwargs": {
# "command": "CLOSED"
# }
# },
# {
# "device_id": vacuum_backbone["gas_valve"],
# "action_name": "set_valve_position",
# "action_kwargs": {
# "command": "OPEN"
# }
# }
# ])
# # 关闭真空泵、打开气源
# vacuum_action_sequence.append([
# {
# "device_id": vacuum_backbone["pump"],
# "action_name": "set_status",
# "action_kwargs": {
# "string": "OFF"
# }
# },
# {
# "device_id": vacuum_backbone["gas"],
# "action_name": "set_status",
# "action_kwargs": {
# "string": "ON"
# }
# }
# ])
# vacuum_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 60}})
# # 关闭气源
# vacuum_action_sequence.append(
# {
# "device_id": vacuum_backbone["gas"],
# "action_name": "set_status",
# "action_kwargs": {
# "string": "OFF"
# }
# }
# )
# # 关闭阀门
# vacuum_action_sequence.append(
# {
# "device_id": vacuum_backbone["gas_valve"],
# "action_name": "set_valve_position",
# "action_kwargs": {
# "command": "CLOSED"
# }
# }
# )
# return vacuum_action_sequence

View File

@@ -1,81 +1,326 @@
import numpy as np from typing import List, Dict, Any
import networkx as nx import networkx as nx
from .pump_protocol import generate_pump_protocol
def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
"""
获取容器中的液体体积
"""
if vessel not in G.nodes():
return 0.0
vessel_data = G.nodes[vessel].get('data', {})
liquids = vessel_data.get('liquid', [])
total_volume = 0.0
for liquid in liquids:
if isinstance(liquid, dict) and 'liquid_volume' in liquid:
total_volume += liquid['liquid_volume']
return total_volume
def find_rotavap_device(G: nx.DiGraph) -> str:
"""查找旋转蒸发仪设备"""
rotavap_nodes = [node for node in G.nodes()
if (G.nodes[node].get('class') or '') == 'virtual_rotavap']
if rotavap_nodes:
return rotavap_nodes[0]
raise ValueError("系统中未找到旋转蒸发仪设备")
def find_solvent_recovery_vessel(G: nx.DiGraph) -> str:
"""查找溶剂回收容器"""
possible_names = [
"flask_distillate",
"bottle_distillate",
"vessel_distillate",
"distillate",
"solvent_recovery",
"flask_solvent_recovery",
"collection_flask"
]
for vessel_name in possible_names:
if vessel_name in G.nodes():
return vessel_name
# 如果找不到专门的回收容器,使用废液容器
waste_names = ["waste_workup", "flask_waste", "bottle_waste", "waste"]
for vessel_name in waste_names:
if vessel_name in G.nodes():
return vessel_name
raise ValueError(f"未找到溶剂回收容器。尝试了以下名称: {possible_names + waste_names}")
def generate_evaporate_protocol( def generate_evaporate_protocol(
G: nx.DiGraph, G: nx.DiGraph,
vessel: str, vessel: str,
pressure: float, pressure: float = 0.1,
temp: float, temp: float = 60.0,
time: float, time: float = 1800.0,
stir_speed: float stir_speed: float = 100.0
) -> list[dict]: ) -> List[Dict[str, Any]]:
""" """
Generate a protocol to evaporate a solution from a vessel. 生成蒸发操作的协议序列
:param G: Directed graph. Nodes are containers and pumps, edges are fluidic connections. 蒸发流程:
:param vessel: Vessel to clean. 1. 液体转移:将待蒸发溶液从源容器转移到旋转蒸发仪
:param solvent: Solvent to clean vessel with. 2. 蒸发操作:执行旋转蒸发
:param volume: Volume of solvent to clean vessel with. 3. (可选) 溶剂回收:将冷凝的溶剂转移到回收容器
:param temp: Temperature to heat vessel to while cleaning. 4. 残留物转移:将浓缩物从旋转蒸发仪转移回原容器或新容器
:param repeats: Number of cleaning cycles to perform.
:return: List of actions to clean vessel. Args:
G: 有向图,节点为设备和容器,边为流体管道
vessel: 包含待蒸发溶液的容器名称
pressure: 蒸发时的真空度 (bar)默认0.1 bar
temp: 蒸发时的加热温度 (°C)默认60°C
time: 蒸发时间 (秒)默认1800秒(30分钟)
stir_speed: 旋转速度 (RPM)默认100 RPM
Returns:
List[Dict[str, Any]]: 蒸发操作的动作序列
Raises:
ValueError: 当找不到必要的设备时抛出异常
Examples:
evaporate_actions = generate_evaporate_protocol(G, "reaction_mixture", 0.05, 80.0, 3600.0)
""" """
action_sequence = []
# 生成泵操作的动作序列 print(f"EVAPORATE: 开始生成蒸发协议")
pump_action_sequence = [] print(f" - 源容器: {vessel}")
reactor_volume = 500.0 print(f" - 真空度: {pressure} bar")
transfer_flowrate = flowrate = 2.5 print(f" - 温度: {temp}°C")
print(f" - 时间: {time}s ({time/60:.1f}分钟)")
print(f" - 旋转速度: {stir_speed} RPM")
# 开启冷凝器 # 验证源容器存在
pump_action_sequence.append({ if vessel not in G.nodes():
"device_id": "rotavap_chiller", raise ValueError(f"源容器 '{vessel}' 不存在于系统中")
"action_name": "set_temperature",
"action_kwargs": { # 获取源容器中的液体体积
"command": "-40" source_volume = get_vessel_liquid_volume(G, vessel)
} print(f"EVAPORATE: 源容器 {vessel} 中有 {source_volume} mL 液体")
})
# TODO: 通过温度反馈改为 HeatChillToTemp而非等待固定时间 # 查找旋转蒸发仪
pump_action_sequence.append({ try:
rotavap_id = find_rotavap_device(G)
print(f"EVAPORATE: 找到旋转蒸发仪: {rotavap_id}")
except ValueError as e:
raise ValueError(f"无法找到旋转蒸发仪: {str(e)}")
# 查找旋转蒸发仪样品容器
rotavap_vessel = None
possible_rotavap_vessels = ["rotavap_flask", "rotavap", "flask_rotavap", "evaporation_flask"]
for rv in possible_rotavap_vessels:
if rv in G.nodes():
rotavap_vessel = rv
break
if not rotavap_vessel:
raise ValueError(f"未找到旋转蒸发仪样品容器。尝试了: {possible_rotavap_vessels}")
print(f"EVAPORATE: 找到旋转蒸发仪样品容器: {rotavap_vessel}")
# 查找溶剂回收容器
try:
distillate_vessel = find_solvent_recovery_vessel(G)
print(f"EVAPORATE: 找到溶剂回收容器: {distillate_vessel}")
except ValueError as e:
print(f"EVAPORATE: 警告 - {str(e)}")
distillate_vessel = None
# === 简化的体积计算策略 ===
if source_volume > 0:
# 如果能检测到液体体积,使用实际体积的大部分
transfer_volume = min(source_volume * 0.9, 250.0) # 90%或最多250mL
print(f"EVAPORATE: 检测到液体体积,将转移 {transfer_volume} mL")
else:
# 如果检测不到液体体积,默认转移一整瓶 250mL
transfer_volume = 250.0
print(f"EVAPORATE: 未检测到液体体积,默认转移整瓶 {transfer_volume} mL")
# === 第一步:将待蒸发溶液转移到旋转蒸发仪 ===
print(f"EVAPORATE: 将 {transfer_volume} mL 溶液从 {vessel} 转移到 {rotavap_vessel}")
try:
transfer_to_rotavap_actions = generate_pump_protocol(
G=G,
from_vessel=vessel,
to_vessel=rotavap_vessel,
volume=transfer_volume,
flowrate=2.0,
transfer_flowrate=2.0
)
action_sequence.extend(transfer_to_rotavap_actions)
except Exception as e:
raise ValueError(f"无法将溶液转移到旋转蒸发仪: {str(e)}")
# 转移后等待
wait_action = {
"action_name": "wait", "action_name": "wait",
"action_kwargs": { "action_kwargs": {"time": 10}
"time": 1800 }
} action_sequence.append(wait_action)
})
# 开启旋蒸真空泵、旋转在液体转移后运行time时间 # === 第二步:执行旋转蒸发 ===
pump_action_sequence.append({ print(f"EVAPORATE: 执行旋转蒸发操作")
"device_id": "rotavap_controller", evaporate_action = {
"action_name": "set_pump_time", "device_id": rotavap_id,
"action_name": "evaporate",
"action_kwargs": { "action_kwargs": {
"command": str(time + reactor_volume / flowrate * 3) "vessel": rotavap_vessel,
"pressure": pressure,
"temp": temp,
"time": time,
"stir_speed": stir_speed
} }
}) }
pump_action_sequence.append({ action_sequence.append(evaporate_action)
"device_id": "rotavap_controller",
"action_name": "set_pump_time",
"action_kwargs": {
"command": str(time + reactor_volume / flowrate * 3)
}
})
# 液体转入旋转蒸发器 # 蒸发后等待系统稳定
pump_action_sequence.append({ wait_action = {
"device_id": "",
"action_name": "PumpTransferProtocol",
"action_kwargs": {
"from_vessel": vessel,
"to_vessel": "rotavap",
"volume": reactor_volume,
"time": reactor_volume / flowrate,
# "transfer_flowrate": transfer_flowrate,
}
})
pump_action_sequence.append({
"action_name": "wait", "action_name": "wait",
"action_kwargs": { "action_kwargs": {"time": 30}
"time": time }
} action_sequence.append(wait_action)
})
return pump_action_sequence # === 第三步:溶剂回收(如果有回收容器)===
if distillate_vessel:
print(f"EVAPORATE: 回收冷凝溶剂到 {distillate_vessel}")
try:
condenser_vessel = "rotavap_condenser"
if condenser_vessel in G.nodes():
# 估算回收体积约为转移体积的70% - 大部分溶剂被蒸发回收)
recovery_volume = transfer_volume * 0.7
print(f"EVAPORATE: 预计回收 {recovery_volume} mL 溶剂")
recovery_actions = generate_pump_protocol(
G=G,
from_vessel=condenser_vessel,
to_vessel=distillate_vessel,
volume=recovery_volume,
flowrate=3.0,
transfer_flowrate=3.0
)
action_sequence.extend(recovery_actions)
else:
print("EVAPORATE: 未找到冷凝器容器,跳过溶剂回收")
except Exception as e:
print(f"EVAPORATE: 溶剂回收失败: {str(e)}")
# === 第四步:将浓缩物转移回原容器 ===
print(f"EVAPORATE: 将浓缩物从旋转蒸发仪转移回 {vessel}")
try:
# 估算浓缩物体积约为转移体积的20% - 大部分溶剂已蒸发)
concentrate_volume = transfer_volume * 0.2
print(f"EVAPORATE: 预计浓缩物体积 {concentrate_volume} mL")
transfer_back_actions = generate_pump_protocol(
G=G,
from_vessel=rotavap_vessel,
to_vessel=vessel,
volume=concentrate_volume,
flowrate=1.0, # 浓缩物可能粘稠,用较慢流速
transfer_flowrate=1.0
)
action_sequence.extend(transfer_back_actions)
except Exception as e:
print(f"EVAPORATE: 将浓缩物转移回容器失败: {str(e)}")
# === 第五步:清洗旋转蒸发仪 ===
print(f"EVAPORATE: 清洗旋转蒸发仪")
try:
# 查找清洗溶剂
cleaning_solvent = None
for solvent in ["flask_ethanol", "flask_acetone", "flask_water"]:
if solvent in G.nodes():
cleaning_solvent = solvent
break
if cleaning_solvent and distillate_vessel:
# 用固定量溶剂清洗(不依赖检测体积)
cleaning_volume = 50.0 # 固定50mL清洗
print(f"EVAPORATE: 用 {cleaning_volume} mL {cleaning_solvent} 清洗")
# 清洗溶剂加入
cleaning_actions = generate_pump_protocol(
G=G,
from_vessel=cleaning_solvent,
to_vessel=rotavap_vessel,
volume=cleaning_volume,
flowrate=2.0,
transfer_flowrate=2.0
)
action_sequence.extend(cleaning_actions)
# 将清洗液转移到废液/回收容器
waste_actions = generate_pump_protocol(
G=G,
from_vessel=rotavap_vessel,
to_vessel=distillate_vessel, # 使用回收容器作为废液
volume=cleaning_volume,
flowrate=2.0,
transfer_flowrate=2.0
)
action_sequence.extend(waste_actions)
except Exception as e:
print(f"EVAPORATE: 清洗步骤失败: {str(e)}")
print(f"EVAPORATE: 生成了 {len(action_sequence)} 个动作")
print(f"EVAPORATE: 蒸发协议生成完成")
print(f"EVAPORATE: 总处理体积: {transfer_volume} mL")
return action_sequence
# 便捷函数:常用蒸发方案 - 都使用250mL标准瓶体积
def generate_quick_evaporate_protocol(
G: nx.DiGraph,
vessel: str,
temp: float = 40.0,
time: float = 900.0 # 15分钟
) -> List[Dict[str, Any]]:
"""快速蒸发:低温、短时间、整瓶处理"""
return generate_evaporate_protocol(G, vessel, 0.2, temp, time, 80.0)
def generate_gentle_evaporate_protocol(
G: nx.DiGraph,
vessel: str,
temp: float = 50.0,
time: float = 2700.0 # 45分钟
) -> List[Dict[str, Any]]:
"""温和蒸发:中等条件、较长时间、整瓶处理"""
return generate_evaporate_protocol(G, vessel, 0.1, temp, time, 60.0)
def generate_high_vacuum_evaporate_protocol(
G: nx.DiGraph,
vessel: str,
temp: float = 35.0,
time: float = 3600.0 # 1小时
) -> List[Dict[str, Any]]:
"""高真空蒸发:低温、高真空、长时间、整瓶处理"""
return generate_evaporate_protocol(G, vessel, 0.01, temp, time, 120.0)
def generate_standard_evaporate_protocol(
G: nx.DiGraph,
vessel: str
) -> List[Dict[str, Any]]:
"""标准蒸发常用参数、整瓶250mL处理"""
return generate_evaporate_protocol(
G=G,
vessel=vessel,
pressure=0.1, # 标准真空度
temp=60.0, # 适中温度
time=1800.0, # 30分钟
stir_speed=100.0 # 适中旋转速度
)

View File

@@ -0,0 +1,304 @@
from typing import List, Dict, Any
import networkx as nx
from .pump_protocol import generate_pump_protocol
def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
"""获取容器中的液体体积"""
if vessel not in G.nodes():
return 0.0
vessel_data = G.nodes[vessel].get('data', {})
liquids = vessel_data.get('liquid', [])
total_volume = 0.0
for liquid in liquids:
if isinstance(liquid, dict) and 'liquid_volume' in liquid:
total_volume += liquid['liquid_volume']
return total_volume
def find_filter_device(G: nx.DiGraph) -> str:
"""查找过滤器设备"""
filter_nodes = [node for node in G.nodes()
if (G.nodes[node].get('class') or '') == 'virtual_filter']
if filter_nodes:
return filter_nodes[0]
raise ValueError("系统中未找到过滤器设备")
def find_filter_vessel(G: nx.DiGraph) -> str:
"""查找过滤器专用容器"""
possible_names = [
"filter_vessel", # 标准过滤器容器
"filtration_vessel", # 备选名称
"vessel_filter", # 备选名称
"filter_unit", # 备选名称
"filter" # 简单名称
]
for vessel_name in possible_names:
if vessel_name in G.nodes():
return vessel_name
raise ValueError(f"未找到过滤器容器。尝试了以下名称: {possible_names}")
def find_filtrate_vessel(G: nx.DiGraph, filtrate_vessel: str = "") -> str:
"""查找滤液收集容器"""
if filtrate_vessel and filtrate_vessel in G.nodes():
return filtrate_vessel
# 自动查找滤液容器
possible_names = [
"filtrate_vessel",
"collection_bottle_1",
"collection_bottle_2",
"waste_workup"
]
for vessel_name in possible_names:
if vessel_name in G.nodes():
return vessel_name
raise ValueError(f"未找到滤液收集容器。尝试了以下名称: {possible_names}")
def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str:
"""查找与指定容器相连的加热搅拌器"""
# 查找所有加热搅拌器节点
heatchill_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_heatchill']
# 检查哪个加热器与目标容器相连
for heatchill in heatchill_nodes:
if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill):
return heatchill
# 如果没有直接连接,返回第一个可用的加热器
if heatchill_nodes:
return heatchill_nodes[0]
raise ValueError(f"未找到与容器 {vessel} 相连的加热搅拌器")
def generate_filter_protocol(
G: nx.DiGraph,
vessel: str,
filtrate_vessel: str = "",
stir: bool = False,
stir_speed: float = 300.0,
temp: float = 25.0,
continue_heatchill: bool = False,
volume: float = 0.0
) -> List[Dict[str, Any]]:
"""
生成过滤操作的协议序列,复用 pump_protocol 的成熟算法
过滤流程:
1. 液体转移:将待过滤溶液从源容器转移到过滤器
2. 启动加热搅拌:设置温度和搅拌
3. 执行过滤:通过过滤器分离固液
4. (可选) 继续或停止加热搅拌
Args:
G: 有向图,节点为设备和容器,边为流体管道
vessel: 包含待过滤溶液的容器名称
filtrate_vessel: 滤液收集容器(可选,自动查找)
stir: 是否在过滤过程中搅拌
stir_speed: 搅拌速度 (RPM)
temp: 过滤温度 (°C)
continue_heatchill: 过滤后是否继续加热搅拌
volume: 预期过滤体积 (mL)0表示全部过滤
Returns:
List[Dict[str, Any]]: 过滤操作的动作序列
"""
action_sequence = []
print(f"FILTER: 开始生成过滤协议")
print(f" - 源容器: {vessel}")
print(f" - 滤液容器: {filtrate_vessel}")
print(f" - 搅拌: {stir} ({stir_speed} RPM)" if stir else " - 搅拌: 否")
print(f" - 过滤温度: {temp}°C")
print(f" - 预期过滤体积: {volume} mL" if volume > 0 else " - 预期过滤体积: 全部")
print(f" - 继续加热搅拌: {continue_heatchill}")
# 验证源容器存在
if vessel not in G.nodes():
raise ValueError(f"源容器 '{vessel}' 不存在于系统中")
# 获取源容器中的液体体积
source_volume = get_vessel_liquid_volume(G, vessel)
print(f"FILTER: 源容器 {vessel} 中有 {source_volume} mL 液体")
# 查找过滤器设备
try:
filter_id = find_filter_device(G)
print(f"FILTER: 找到过滤器: {filter_id}")
except ValueError as e:
raise ValueError(f"无法找到过滤器: {str(e)}")
# 查找过滤器容器
try:
filter_vessel_id = find_filter_vessel(G)
print(f"FILTER: 找到过滤器容器: {filter_vessel_id}")
except ValueError as e:
raise ValueError(f"无法找到过滤器容器: {str(e)}")
# 查找滤液收集容器
try:
actual_filtrate_vessel = find_filtrate_vessel(G, filtrate_vessel)
print(f"FILTER: 找到滤液收集容器: {actual_filtrate_vessel}")
except ValueError as e:
raise ValueError(f"无法找到滤液收集容器: {str(e)}")
# 查找加热搅拌器(如果需要温度控制或搅拌)
heatchill_id = None
if temp != 25.0 or stir or continue_heatchill:
try:
heatchill_id = find_connected_heatchill(G, filter_vessel_id)
print(f"FILTER: 找到加热搅拌器: {heatchill_id}")
except ValueError as e:
print(f"FILTER: 警告 - {str(e)}")
# === 简化的体积计算策略 ===
if volume > 0:
transfer_volume = min(volume, source_volume if source_volume > 0 else volume)
print(f"FILTER: 指定过滤体积 {transfer_volume} mL")
elif source_volume > 0:
transfer_volume = source_volume * 0.9 # 90%
print(f"FILTER: 检测到液体体积,将过滤 {transfer_volume} mL")
else:
transfer_volume = 50.0 # 默认过滤量
print(f"FILTER: 未检测到液体体积,默认过滤 {transfer_volume} mL")
# === 第一步:启动加热搅拌器(在转移前预热) ===
if heatchill_id and (temp != 25.0 or stir):
print(f"FILTER: 启动加热搅拌器,温度: {temp}°C搅拌: {stir}")
heatchill_action = {
"device_id": heatchill_id,
"action_name": "heat_chill_start",
"action_kwargs": {
"vessel": filter_vessel_id,
"temp": temp,
"purpose": f"过滤过程温度控制和搅拌"
}
}
action_sequence.append(heatchill_action)
# 等待温度稳定
if temp != 25.0:
wait_time = min(30, abs(temp - 25.0) * 1.0) # 根据温差估算预热时间
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": wait_time}
})
# === 第二步:将待过滤溶液转移到过滤器 ===
print(f"FILTER: 将 {transfer_volume} mL 溶液从 {vessel} 转移到 {filter_vessel_id}")
try:
# 使用成熟的 pump_protocol 算法进行液体转移
transfer_to_filter_actions = generate_pump_protocol(
G=G,
from_vessel=vessel,
to_vessel=filter_vessel_id,
volume=transfer_volume,
flowrate=1.0, # 过滤转移用较慢速度,避免扰动
transfer_flowrate=1.5
)
action_sequence.extend(transfer_to_filter_actions)
except Exception as e:
raise ValueError(f"无法将溶液转移到过滤器: {str(e)}")
# 转移后等待
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 5}
})
# === 第三步:执行过滤操作(完全按照 Filter.action 参数) ===
print(f"FILTER: 执行过滤操作")
filter_action = {
"device_id": filter_id,
"action_name": "filter",
"action_kwargs": {
"vessel": filter_vessel_id,
"filtrate_vessel": actual_filtrate_vessel,
"stir": stir,
"stir_speed": stir_speed,
"temp": temp,
"continue_heatchill": continue_heatchill,
"volume": transfer_volume
}
}
action_sequence.append(filter_action)
# 过滤后等待
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 10}
})
# === 第四步:如果不继续加热搅拌,停止加热器 ===
if heatchill_id and not continue_heatchill and (temp != 25.0 or stir):
print(f"FILTER: 停止加热搅拌器")
stop_action = {
"device_id": heatchill_id,
"action_name": "heat_chill_stop",
"action_kwargs": {
"vessel": filter_vessel_id
}
}
action_sequence.append(stop_action)
print(f"FILTER: 生成了 {len(action_sequence)} 个动作")
print(f"FILTER: 过滤协议生成完成")
return action_sequence
# 便捷函数:常用过滤方案
def generate_gravity_filter_protocol(
G: nx.DiGraph,
vessel: str,
filtrate_vessel: str = ""
) -> List[Dict[str, Any]]:
"""重力过滤:室温,无搅拌"""
return generate_filter_protocol(G, vessel, filtrate_vessel, False, 0.0, 25.0, False, 0.0)
def generate_hot_filter_protocol(
G: nx.DiGraph,
vessel: str,
filtrate_vessel: str = "",
temp: float = 60.0
) -> List[Dict[str, Any]]:
"""热过滤:高温过滤,防止结晶析出"""
return generate_filter_protocol(G, vessel, filtrate_vessel, False, 0.0, temp, False, 0.0)
def generate_stirred_filter_protocol(
G: nx.DiGraph,
vessel: str,
filtrate_vessel: str = "",
stir_speed: float = 200.0
) -> List[Dict[str, Any]]:
"""搅拌过滤:低速搅拌,防止滤饼堵塞"""
return generate_filter_protocol(G, vessel, filtrate_vessel, True, stir_speed, 25.0, False, 0.0)
def generate_hot_stirred_filter_protocol(
G: nx.DiGraph,
vessel: str,
filtrate_vessel: str = "",
temp: float = 60.0,
stir_speed: float = 300.0
) -> List[Dict[str, Any]]:
"""热搅拌过滤:高温搅拌过滤"""
return generate_filter_protocol(G, vessel, filtrate_vessel, True, stir_speed, temp, False, 0.0)

View File

@@ -0,0 +1,387 @@
from typing import List, Dict, Any
import networkx as nx
from .pump_protocol import generate_pump_protocol
def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
"""获取容器中的液体体积"""
if vessel not in G.nodes():
return 0.0
vessel_data = G.nodes[vessel].get('data', {})
liquids = vessel_data.get('liquid', [])
total_volume = 0.0
for liquid in liquids:
if isinstance(liquid, dict) and 'liquid_volume' in liquid:
total_volume += liquid['liquid_volume']
return total_volume
def find_filter_through_vessel(G: nx.DiGraph, filter_through: str) -> str:
"""查找过滤介质容器"""
# 直接使用 filter_through 参数作为容器名称
if filter_through in G.nodes():
return filter_through
# 尝试常见的过滤介质容器命名
possible_names = [
f"filter_{filter_through}",
f"{filter_through}_filter",
f"column_{filter_through}",
f"{filter_through}_column",
"filter_through_vessel",
"column_vessel",
"chromatography_column",
"filter_column"
]
for vessel_name in possible_names:
if vessel_name in G.nodes():
return vessel_name
raise ValueError(f"未找到过滤介质容器 '{filter_through}'。尝试了以下名称: {[filter_through] + possible_names}")
def find_eluting_solvent_vessel(G: nx.DiGraph, eluting_solvent: str) -> str:
"""查找洗脱溶剂容器"""
if not eluting_solvent:
return ""
# 按照命名规则查找溶剂瓶
solvent_vessel_id = f"flask_{eluting_solvent}"
if solvent_vessel_id in G.nodes():
return solvent_vessel_id
# 如果直接匹配失败,尝试模糊匹配
for node in G.nodes():
if node.startswith('flask_') and eluting_solvent.lower() in node.lower():
return node
# 如果还是找不到,列出所有可用的溶剂瓶
available_flasks = [node for node in G.nodes()
if node.startswith('flask_')
and G.nodes[node].get('type') == 'container']
raise ValueError(f"找不到洗脱溶剂 '{eluting_solvent}' 对应的溶剂瓶。可用溶剂瓶: {available_flasks}")
def generate_filter_through_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
filter_through: str,
eluting_solvent: str = "",
eluting_volume: float = 0.0,
eluting_repeats: int = 0,
residence_time: float = 0.0
) -> List[Dict[str, Any]]:
"""
生成通过过滤介质过滤的协议序列,复用 pump_protocol 的成熟算法
过滤流程:
1. 液体转移:将样品从源容器转移到过滤介质
2. 重力过滤:液体通过过滤介质自动流到目标容器
3. 洗脱操作:将洗脱溶剂通过过滤介质洗脱目标物质
Args:
G: 有向图,节点为设备和容器,边为流体管道
from_vessel: 源容器的名称,即物质起始所在的容器
to_vessel: 目标容器的名称,物质过滤后要到达的容器
filter_through: 过滤时所通过的介质,如滤纸、柱子等
eluting_solvent: 洗脱溶剂的名称,可选参数
eluting_volume: 洗脱溶剂的体积,可选参数
eluting_repeats: 洗脱操作的重复次数,默认为 0
residence_time: 物质在过滤介质中的停留时间,可选参数
Returns:
List[Dict[str, Any]]: 过滤操作的动作序列
Raises:
ValueError: 当找不到必要的设备或容器时
Examples:
filter_through_actions = generate_filter_through_protocol(
G, "reaction_mixture", "collection_bottle_1", "celite", "ethanol", 20.0, 2, 30.0
)
"""
action_sequence = []
print(f"FILTER_THROUGH: 开始生成通过过滤协议")
print(f" - 源容器: {from_vessel}")
print(f" - 目标容器: {to_vessel}")
print(f" - 过滤介质: {filter_through}")
print(f" - 洗脱溶剂: {eluting_solvent}")
print(f" - 洗脱体积: {eluting_volume} mL" if eluting_volume > 0 else " - 洗脱体积: 无")
print(f" - 洗脱重复次数: {eluting_repeats}")
print(f" - 停留时间: {residence_time}s" if residence_time > 0 else " - 停留时间: 无")
# 验证源容器和目标容器存在
if from_vessel not in G.nodes():
raise ValueError(f"源容器 '{from_vessel}' 不存在于系统中")
if to_vessel not in G.nodes():
raise ValueError(f"目标容器 '{to_vessel}' 不存在于系统中")
# 获取源容器中的液体体积
source_volume = get_vessel_liquid_volume(G, from_vessel)
print(f"FILTER_THROUGH: 源容器 {from_vessel} 中有 {source_volume} mL 液体")
# 查找过滤介质容器
try:
filter_through_vessel = find_filter_through_vessel(G, filter_through)
print(f"FILTER_THROUGH: 找到过滤介质容器: {filter_through_vessel}")
except ValueError as e:
raise ValueError(f"无法找到过滤介质容器: {str(e)}")
# 查找洗脱溶剂容器(如果需要)
eluting_vessel = ""
if eluting_solvent and eluting_volume > 0 and eluting_repeats > 0:
try:
eluting_vessel = find_eluting_solvent_vessel(G, eluting_solvent)
print(f"FILTER_THROUGH: 找到洗脱溶剂容器: {eluting_vessel}")
except ValueError as e:
raise ValueError(f"无法找到洗脱溶剂容器: {str(e)}")
# === 第一步:将样品从源容器转移到过滤介质 ===
transfer_volume = source_volume if source_volume > 0 else 100.0 # 默认100mL
print(f"FILTER_THROUGH: 将 {transfer_volume} mL 样品从 {from_vessel} 转移到 {filter_through_vessel}")
try:
# 使用成熟的 pump_protocol 算法进行液体转移
sample_transfer_actions = generate_pump_protocol(
G=G,
from_vessel=from_vessel,
to_vessel=filter_through_vessel,
volume=transfer_volume,
flowrate=0.8, # 较慢的流速,避免冲击过滤介质
transfer_flowrate=1.2
)
action_sequence.extend(sample_transfer_actions)
except Exception as e:
raise ValueError(f"无法将样品转移到过滤介质: {str(e)}")
# === 第二步:等待样品通过过滤介质(停留时间) ===
if residence_time > 0:
print(f"FILTER_THROUGH: 等待样品在过滤介质中停留 {residence_time}s")
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": residence_time}
})
else:
# 即使没有指定停留时间,也等待一段时间让液体通过
default_wait_time = max(10, transfer_volume / 10) # 根据体积估算等待时间
print(f"FILTER_THROUGH: 等待样品通过过滤介质 {default_wait_time}s")
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": default_wait_time}
})
# === 第三步:洗脱操作(如果指定了洗脱参数) ===
if eluting_solvent and eluting_volume > 0 and eluting_repeats > 0 and eluting_vessel:
print(f"FILTER_THROUGH: 开始洗脱操作 - {eluting_repeats} 次,每次 {eluting_volume} mL {eluting_solvent}")
for repeat_idx in range(eluting_repeats):
print(f"FILTER_THROUGH: 第 {repeat_idx + 1}/{eluting_repeats} 次洗脱")
try:
# 将洗脱溶剂转移到过滤介质
eluting_transfer_actions = generate_pump_protocol(
G=G,
from_vessel=eluting_vessel,
to_vessel=filter_through_vessel,
volume=eluting_volume,
flowrate=0.6, # 洗脱用更慢的流速
transfer_flowrate=1.0
)
action_sequence.extend(eluting_transfer_actions)
except Exception as e:
raise ValueError(f"{repeat_idx + 1} 次洗脱转移失败: {str(e)}")
# 等待洗脱溶剂通过过滤介质
eluting_wait_time = max(30, eluting_volume / 5) # 根据洗脱体积估算等待时间
print(f"FILTER_THROUGH: 等待第 {repeat_idx + 1} 次洗脱液通过 {eluting_wait_time}s")
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": eluting_wait_time}
})
# 洗脱间隔等待
if repeat_idx < eluting_repeats - 1: # 不是最后一次洗脱
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 10}
})
# === 第四步:最终等待,确保所有液体完全通过 ===
print(f"FILTER_THROUGH: 最终等待,确保所有液体完全通过过滤介质")
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 20}
})
print(f"FILTER_THROUGH: 生成了 {len(action_sequence)} 个动作")
print(f"FILTER_THROUGH: 通过过滤协议生成完成")
print(f"FILTER_THROUGH: 样品从 {from_vessel} 通过 {filter_through} 到达 {to_vessel}")
if eluting_repeats > 0:
total_eluting_volume = eluting_volume * eluting_repeats
print(f"FILTER_THROUGH: 总洗脱体积: {total_eluting_volume} mL {eluting_solvent}")
return action_sequence
# 便捷函数:常用过滤方案
def generate_gravity_column_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
column_material: str = "silica_gel"
) -> List[Dict[str, Any]]:
"""重力柱层析:简单重力过滤,无洗脱"""
return generate_filter_through_protocol(G, from_vessel, to_vessel, column_material, "", 0.0, 0, 0.0)
def generate_celite_filter_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
wash_solvent: str = "ethanol",
wash_volume: float = 20.0
) -> List[Dict[str, Any]]:
"""硅藻土过滤:用于去除固体杂质"""
return generate_filter_through_protocol(G, from_vessel, to_vessel, "celite", wash_solvent, wash_volume, 1, 30.0)
def generate_column_chromatography_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
column_material: str = "silica_gel",
eluting_solvent: str = "ethyl_acetate",
eluting_volume: float = 30.0,
eluting_repeats: int = 3
) -> List[Dict[str, Any]]:
"""柱层析:多次洗脱分离"""
return generate_filter_through_protocol(
G, from_vessel, to_vessel, column_material, eluting_solvent, eluting_volume, eluting_repeats, 60.0
)
def generate_solid_phase_extraction_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
spe_cartridge: str = "C18",
eluting_solvent: str = "methanol",
eluting_volume: float = 15.0,
eluting_repeats: int = 2
) -> List[Dict[str, Any]]:
"""固相萃取C18柱或其他SPE柱"""
return generate_filter_through_protocol(
G, from_vessel, to_vessel, spe_cartridge, eluting_solvent, eluting_volume, eluting_repeats, 120.0
)
def generate_resin_filter_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
resin_type: str = "ion_exchange",
regeneration_solvent: str = "NaCl_solution",
regeneration_volume: float = 25.0
) -> List[Dict[str, Any]]:
"""树脂过滤:离子交换树脂或其他功能树脂"""
return generate_filter_through_protocol(
G, from_vessel, to_vessel, resin_type, regeneration_solvent, regeneration_volume, 1, 180.0
)
def generate_multi_step_purification_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
filter_steps: List[Dict[str, Any]]
) -> List[Dict[str, Any]]:
"""
多步骤纯化:连续多个过滤介质
Args:
G: 网络图
from_vessel: 源容器
to_vessel: 最终目标容器
filter_steps: 过滤步骤列表,每个元素包含过滤参数
Returns:
List[Dict[str, Any]]: 完整的动作序列
Example:
filter_steps = [
{
"to_vessel": "intermediate_vessel_1",
"filter_through": "celite",
"eluting_solvent": "",
"eluting_volume": 0.0,
"eluting_repeats": 0,
"residence_time": 30.0
},
{
"from_vessel": "intermediate_vessel_1",
"to_vessel": "final_vessel",
"filter_through": "silica_gel",
"eluting_solvent": "ethyl_acetate",
"eluting_volume": 20.0,
"eluting_repeats": 2,
"residence_time": 60.0
}
]
"""
action_sequence = []
current_from_vessel = from_vessel
for i, step in enumerate(filter_steps):
print(f"FILTER_THROUGH: 处理第 {i+1}/{len(filter_steps)} 个过滤步骤")
# 使用步骤中指定的参数,或使用默认值
step_from_vessel = step.get('from_vessel', current_from_vessel)
step_to_vessel = step.get('to_vessel', to_vessel if i == len(filter_steps) - 1 else f"intermediate_vessel_{i+1}")
# 生成单个过滤步骤的协议
step_actions = generate_filter_through_protocol(
G=G,
from_vessel=step_from_vessel,
to_vessel=step_to_vessel,
filter_through=step.get('filter_through', 'silica_gel'),
eluting_solvent=step.get('eluting_solvent', ''),
eluting_volume=step.get('eluting_volume', 0.0),
eluting_repeats=step.get('eluting_repeats', 0),
residence_time=step.get('residence_time', 0.0)
)
action_sequence.extend(step_actions)
# 更新下一步的源容器
current_from_vessel = step_to_vessel
# 在步骤之间加入等待时间
if i < len(filter_steps) - 1: # 不是最后一个步骤
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 15}
})
print(f"FILTER_THROUGH: 多步骤纯化协议生成完成,共 {len(action_sequence)} 个动作")
return action_sequence
# 测试函数
def test_filter_through_protocol():
"""测试通过过滤协议的示例"""
print("=== FILTER THROUGH PROTOCOL 测试 ===")
print("测试完成")
if __name__ == "__main__":
test_filter_through_protocol()

View File

@@ -0,0 +1,373 @@
from typing import List, Dict, Any, Optional
import networkx as nx
def find_connected_heatchill(G: nx.DiGraph, vessel: str) -> str:
"""
查找与指定容器相连的加热/冷却设备
"""
# 查找所有加热/冷却设备节点
heatchill_nodes = [node for node in G.nodes()
if (G.nodes[node].get('class') or '') == 'virtual_heatchill']
# 检查哪个加热/冷却设备与目标容器相连(机械连接)
for heatchill in heatchill_nodes:
if G.has_edge(heatchill, vessel) or G.has_edge(vessel, heatchill):
return heatchill
# 如果没有直接连接,返回第一个可用的加热/冷却设备
if heatchill_nodes:
return heatchill_nodes[0]
raise ValueError("系统中未找到可用的加热/冷却设备")
def generate_heat_chill_protocol(
G: nx.DiGraph,
vessel: str,
temp: float,
time: float,
stir: bool = False,
stir_speed: float = 300.0,
purpose: str = "加热/冷却操作"
) -> List[Dict[str, Any]]:
"""
生成加热/冷却操作的协议序列 - 带时间限制的完整操作
"""
action_sequence = []
print(f"HEATCHILL: 开始生成加热/冷却协议")
print(f" - 容器: {vessel}")
print(f" - 目标温度: {temp}°C")
print(f" - 持续时间: {time}")
print(f" - 使用内置搅拌: {stir}, 速度: {stir_speed} RPM")
print(f" - 目的: {purpose}")
# 1. 验证容器存在
if vessel not in G.nodes():
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
# 2. 查找加热/冷却设备
try:
heatchill_id = find_connected_heatchill(G, vessel)
print(f"HEATCHILL: 找到加热/冷却设备: {heatchill_id}")
except ValueError as e:
raise ValueError(f"无法找到加热/冷却设备: {str(e)}")
# 3. 执行加热/冷却操作
heatchill_action = {
"device_id": heatchill_id,
"action_name": "heat_chill",
"action_kwargs": {
"vessel": vessel,
"temp": temp,
"time": time,
"stir": stir,
"stir_speed": stir_speed,
"status": "start"
}
}
action_sequence.append(heatchill_action)
print(f"HEATCHILL: 生成了 {len(action_sequence)} 个动作")
return action_sequence
def generate_heat_chill_start_protocol(
G: nx.DiGraph,
vessel: str,
temp: float,
purpose: str = "开始加热/冷却"
) -> List[Dict[str, Any]]:
"""
生成开始加热/冷却操作的协议序列
"""
action_sequence = []
print(f"HEATCHILL_START: 开始生成加热/冷却启动协议")
print(f" - 容器: {vessel}")
print(f" - 目标温度: {temp}°C")
print(f" - 目的: {purpose}")
# 1. 验证容器存在
if vessel not in G.nodes():
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
# 2. 查找加热/冷却设备
try:
heatchill_id = find_connected_heatchill(G, vessel)
print(f"HEATCHILL_START: 找到加热/冷却设备: {heatchill_id}")
except ValueError as e:
raise ValueError(f"无法找到加热/冷却设备: {str(e)}")
# 3. 执行开始加热/冷却操作
heatchill_start_action = {
"device_id": heatchill_id,
"action_name": "heat_chill_start",
"action_kwargs": {
"vessel": vessel,
"temp": temp,
"purpose": purpose
}
}
action_sequence.append(heatchill_start_action)
print(f"HEATCHILL_START: 生成了 {len(action_sequence)} 个动作")
return action_sequence
def generate_heat_chill_stop_protocol(
G: nx.DiGraph,
vessel: str
) -> List[Dict[str, Any]]:
"""
生成停止加热/冷却操作的协议序列
"""
action_sequence = []
print(f"HEATCHILL_STOP: 开始生成加热/冷却停止协议")
print(f" - 容器: {vessel}")
# 1. 验证容器存在
if vessel not in G.nodes():
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
# 2. 查找加热/冷却设备
try:
heatchill_id = find_connected_heatchill(G, vessel)
print(f"HEATCHILL_STOP: 找到加热/冷却设备: {heatchill_id}")
except ValueError as e:
raise ValueError(f"无法找到加热/冷却设备: {str(e)}")
# 3. 执行停止加热/冷却操作
heatchill_stop_action = {
"device_id": heatchill_id,
"action_name": "heat_chill_stop",
"action_kwargs": {
"vessel": vessel
}
}
action_sequence.append(heatchill_stop_action)
print(f"HEATCHILL_STOP: 生成了 {len(action_sequence)} 个动作")
return action_sequence
def generate_heat_chill_to_temp_protocol(
G: nx.DiGraph,
vessel: str,
temp: float,
active: bool = True,
continue_heatchill: bool = False,
stir: bool = False,
stir_speed: Optional[float] = None,
purpose: Optional[str] = None
) -> List[Dict[str, Any]]:
"""
生成加热/冷却到指定温度的协议序列 - 智能温控协议
**关键修复**: 学习 pump_protocol 的模式,直接使用设备基础动作,不依赖特定的 Action 文件
"""
action_sequence = []
# 设置默认值
if stir_speed is None:
stir_speed = 300.0
if purpose is None:
purpose = f"智能温控到 {temp}°C"
print(f"HEATCHILL_TO_TEMP: 开始生成智能温控协议")
print(f" - 容器: {vessel}")
print(f" - 目标温度: {temp}°C")
print(f" - 主动控温: {active}")
print(f" - 达到温度后继续: {continue_heatchill}")
print(f" - 搅拌: {stir}, 速度: {stir_speed} RPM")
print(f" - 目的: {purpose}")
# 1. 验证容器存在
if vessel not in G.nodes():
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
# 2. 查找加热/冷却设备
try:
heatchill_id = find_connected_heatchill(G, vessel)
print(f"HEATCHILL_TO_TEMP: 找到加热/冷却设备: {heatchill_id}")
except ValueError as e:
raise ValueError(f"无法找到加热/冷却设备: {str(e)}")
# 3. 根据参数选择合适的基础动作组合 (学习 pump_protocol 的模式)
if not active:
print(f"HEATCHILL_TO_TEMP: 非主动模式,仅等待")
action_sequence.append({
"action_name": "wait",
"action_kwargs": {
"time": 10.0,
"purpose": f"等待容器 {vessel} 自然达到 {temp}°C"
}
})
else:
if continue_heatchill:
# 持续模式:使用 heat_chill_start 基础动作
print(f"HEATCHILL_TO_TEMP: 使用持续温控模式")
action_sequence.append({
"device_id": heatchill_id,
"action_name": "heat_chill_start", # ← 直接使用设备基础动作
"action_kwargs": {
"vessel": vessel,
"temp": temp,
"purpose": f"{purpose} (持续保温)"
}
})
else:
# 一次性模式:使用 heat_chill 基础动作
print(f"HEATCHILL_TO_TEMP: 使用一次性温控模式")
estimated_time = max(60.0, min(900.0, abs(temp - 25.0) * 30.0))
print(f"HEATCHILL_TO_TEMP: 估算所需时间: {estimated_time}")
action_sequence.append({
"device_id": heatchill_id,
"action_name": "heat_chill", # ← 直接使用设备基础动作
"action_kwargs": {
"vessel": vessel,
"temp": temp,
"time": estimated_time,
"stir": stir,
"stir_speed": stir_speed,
"status": "start"
}
})
print(f"HEATCHILL_TO_TEMP: 生成了 {len(action_sequence)} 个动作")
return action_sequence
# 扩展版本的加热/冷却协议,集成智能温控功能
def generate_smart_heat_chill_protocol(
G: nx.DiGraph,
vessel: str,
temp: float,
time: float = 0.0, # 0表示自动估算
active: bool = True,
continue_heatchill: bool = False,
stir: bool = False,
stir_speed: float = 300.0,
purpose: str = "智能加热/冷却"
) -> List[Dict[str, Any]]:
"""
这个函数集成了 generate_heat_chill_to_temp_protocol 的智能逻辑,
但使用现有的 Action 类型
"""
# 如果时间为0自动估算
if time == 0.0:
estimated_time = max(60.0, min(900.0, abs(temp - 25.0) * 30.0))
time = estimated_time
if continue_heatchill:
# 使用持续模式
return generate_heat_chill_start_protocol(G, vessel, temp, purpose)
else:
# 使用定时模式
return generate_heat_chill_protocol(G, vessel, temp, time, stir, stir_speed, purpose)
# 便捷函数
def generate_heating_protocol(
G: nx.DiGraph,
vessel: str,
temp: float,
time: float = 300.0,
stir: bool = True,
stir_speed: float = 300.0
) -> List[Dict[str, Any]]:
"""生成加热协议的便捷函数"""
return generate_heat_chill_protocol(
G=G, vessel=vessel, temp=temp, time=time,
stir=stir, stir_speed=stir_speed, purpose=f"加热到 {temp}°C"
)
def generate_cooling_protocol(
G: nx.DiGraph,
vessel: str,
temp: float,
time: float = 600.0,
stir: bool = True,
stir_speed: float = 200.0
) -> List[Dict[str, Any]]:
"""生成冷却协议的便捷函数"""
return generate_heat_chill_protocol(
G=G, vessel=vessel, temp=temp, time=time,
stir=stir, stir_speed=stir_speed, purpose=f"冷却到 {temp}°C"
)
# # 温度预设快捷函数
# def generate_room_temp_protocol(
# G: nx.DiGraph,
# vessel: str,
# stir: bool = False
# ) -> List[Dict[str, Any]]:
# """返回室温的快捷函数"""
# return generate_heat_chill_to_temp_protocol(
# G=G,
# vessel=vessel,
# temp=25.0,
# active=True,
# continue_heatchill=False,
# stir=stir,
# purpose="冷却到室温"
# )
# def generate_reflux_heating_protocol(
# G: nx.DiGraph,
# vessel: str,
# temp: float,
# time: float = 3600.0 # 1小时回流
# ) -> List[Dict[str, Any]]:
# """回流加热的快捷函数"""
# return generate_heat_chill_protocol(
# G=G,
# vessel=vessel,
# temp=temp,
# time=time,
# stir=True,
# stir_speed=400.0, # 回流时较快搅拌
# purpose=f"回流加热到 {temp}°C"
# )
# def generate_ice_bath_protocol(
# G: nx.DiGraph,
# vessel: str,
# time: float = 600.0 # 10分钟冰浴
# ) -> List[Dict[str, Any]]:
# """冰浴冷却的快捷函数"""
# return generate_heat_chill_protocol(
# G=G,
# vessel=vessel,
# temp=0.0,
# time=time,
# stir=True,
# stir_speed=150.0, # 冰浴时缓慢搅拌
# purpose="冰浴冷却到 0°C"
# )
# 测试函数
def test_heatchill_protocol():
"""测试加热/冷却协议的示例"""
print("=== HEAT CHILL PROTOCOL 测试 ===")
print("完整的四个协议函数:")
print("1. generate_heat_chill_protocol - 带时间限制的完整操作")
print("2. generate_heat_chill_start_protocol - 持续加热/冷却")
print("3. generate_heat_chill_stop_protocol - 停止加热/冷却")
print("4. generate_heat_chill_to_temp_protocol - 智能温控 (您的 HeatChillToTemp)")
print("测试完成")
if __name__ == "__main__":
test_heatchill_protocol()

View File

@@ -2,17 +2,43 @@ import numpy as np
import networkx as nx 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):
node_class = G.nodes[neighbor].get("class") or "" # 防止 None
if "pump" in node_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( def generate_pump_protocol(
G: nx.DiGraph, G: nx.DiGraph,
from_vessel: str, from_vessel: str,
to_vessel: str, to_vessel: str,
volume: float, volume: float,
flowrate: float = 0.5, flowrate: float = 0.5,
transfer_flowrate: float = 0, transfer_flowrate: float = 0,
) -> list[dict]: ) -> list[dict]:
""" """
生成泵操作的动作序列。 生成泵操作的动作序列。
:param G: 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置 :param G: 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置
:param from_vessel: 容器A :param from_vessel: 容器A
:param to_vessel: 容器B :param to_vessel: 容器B
@@ -21,7 +47,7 @@ def generate_pump_protocol(
:param transfer_flowrate: 泵骨架中转移流速(若不指定,默认与注入流速相同) :param transfer_flowrate: 泵骨架中转移流速(若不指定,默认与注入流速相同)
:return: 泵操作的动作序列 :return: 泵操作的动作序列
""" """
# 生成泵操作的动作序列 # 生成泵操作的动作序列
pump_action_sequence = [] pump_action_sequence = []
nodes = G.nodes(data=True) nodes = G.nodes(data=True)
@@ -34,31 +60,33 @@ def generate_pump_protocol(
pump_backbone = pump_backbone[1:] pump_backbone = pump_backbone[1:]
if not to_vessel.startswith("pump"): if not to_vessel.startswith("pump"):
pump_backbone = pump_backbone[:-1] pump_backbone = pump_backbone[:-1]
if transfer_flowrate == 0: if transfer_flowrate == 0:
transfer_flowrate = flowrate transfer_flowrate = flowrate
min_transfer_volume = min([nodes[pump]["max_volume"] for pump in pump_backbone]) 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)) repeats = int(np.ceil(volume / min_transfer_volume))
if repeats > 1 and (from_vessel.startswith("pump") or to_vessel.startswith("pump")): 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.") raise ValueError("Cannot transfer volume larger than min_transfer_volume between two pumps.")
volume_left = volume volume_left = volume
# 生成泵操作的动作序列 # 生成泵操作的动作序列
for i in range(repeats): for i in range(repeats):
# 单泵依次执行阀指令、活塞指令,将液体吸入与之相连的第一台泵 # 单泵依次执行阀指令、活塞指令,将液体吸入与之相连的第一台泵
if not from_vessel.startswith("pump"): if not from_vessel.startswith("pump"):
pump_action_sequence.extend([ pump_action_sequence.extend([
{ {
"device_id": pump_backbone[0], "device_id": valve_from_node[pump_backbone[0]],
"action_name": "set_valve_position", "action_name": "set_valve_position",
"action_kwargs": { "action_kwargs": {
"command": G.get_edge_data(pump_backbone[0], from_vessel)["port"][pump_backbone[0]] "command": G.get_edge_data(pump_backbone[0], from_vessel)["port"][pump_backbone[0]]
} }
}, },
{ {
"device_id": pump_backbone[0], "device_id": pumps_from_node[pump_backbone[0]],
"action_name": "set_position", "action_name": "set_position",
"action_kwargs": { "action_kwargs": {
"position": float(min(volume_left, min_transfer_volume)), "position": float(min(volume_left, min_transfer_volume)),
@@ -67,57 +95,57 @@ def generate_pump_protocol(
} }
]) ])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
for pumpA, pumpB in zip(pump_backbone[:-1], pump_backbone[1:]): for nodeA, nodeB in zip(pump_backbone[:-1], pump_backbone[1:]):
# 相邻两泵同时切换阀门至连通位置 # 相邻两泵同时切换阀门至连通位置
pump_action_sequence.append([ pump_action_sequence.append([
{ {
"device_id": pumpA, "device_id": valve_from_node[nodeA],
"action_name": "set_valve_position", "action_name": "set_valve_position",
"action_kwargs": { "action_kwargs": {
"command": G.get_edge_data(pumpA, pumpB)["port"][pumpA] "command": G.get_edge_data(nodeA, nodeB)["port"][nodeA]
}
},
{
"device_id": valve_from_node[nodeB],
"action_name": "set_valve_position",
"action_kwargs": {
"command": G.get_edge_data(nodeB, nodeA)["port"][nodeB],
}
} }
},
{
"device_id": pumpB,
"action_name": "set_valve_position",
"action_kwargs": {
"command": G.get_edge_data(pumpB, pumpA)["port"][pumpB],
}
}
]) ])
# 相邻两泵液体转移泵A排出液体泵B吸入液体 # 相邻两泵液体转移泵A排出液体泵B吸入液体
pump_action_sequence.append([ pump_action_sequence.append([
{ {
"device_id": pumpA, "device_id": pumps_from_node[nodeA],
"action_name": "set_position", "action_name": "set_position",
"action_kwargs": { "action_kwargs": {
"position": 0.0, "position": 0.0,
"max_velocity": transfer_flowrate "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
}
} }
},
{
"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}}) pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
if not to_vessel.startswith("pump"): if not to_vessel.startswith("pump"):
# 单泵依次执行阀指令、活塞指令将最后一台泵液体缓慢加入容器B # 单泵依次执行阀指令、活塞指令将最后一台泵液体缓慢加入容器B
pump_action_sequence.extend([ pump_action_sequence.extend([
{ {
"device_id": pump_backbone[-1], "device_id": valve_from_node[pump_backbone[-1]],
"action_name": "set_valve_position", "action_name": "set_valve_position",
"action_kwargs": { "action_kwargs": {
"command": G.get_edge_data(pump_backbone[-1], to_vessel)["port"][pump_backbone[-1]] "command": G.get_edge_data(pump_backbone[-1], to_vessel)["port"][pump_backbone[-1]]
} }
}, },
{ {
"device_id": pump_backbone[-1], "device_id": pumps_from_node[pump_backbone[-1]],
"action_name": "set_position", "action_name": "set_position",
"action_kwargs": { "action_kwargs": {
"position": 0.0, "position": 0.0,
@@ -126,30 +154,30 @@ def generate_pump_protocol(
} }
]) ])
pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}}) pump_action_sequence.append({"action_name": "wait", "action_kwargs": {"time": 5}})
volume_left -= min_transfer_volume volume_left -= min_transfer_volume
return pump_action_sequence return pump_action_sequence
# Pump protocol compilation # Pump protocol compilation
def generate_pump_protocol_with_rinsing( def generate_pump_protocol_with_rinsing(
G: nx.DiGraph, G: nx.DiGraph,
from_vessel: str, from_vessel: str,
to_vessel: str, to_vessel: str,
volume: float, volume: float,
amount: str = "", amount: str = "",
time: float = 0, time: float = 0,
viscous: bool = False, viscous: bool = False,
rinsing_solvent: str = "air", rinsing_solvent: str = "air",
rinsing_volume: float = 5.0, rinsing_volume: float = 5.0,
rinsing_repeats: int = 2, rinsing_repeats: int = 2,
solid: bool = False, solid: bool = False,
flowrate: float = 2.5, flowrate: float = 2.5,
transfer_flowrate: float = 0.5, transfer_flowrate: float = 0.5,
) -> list[dict]: ) -> 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. 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: Args:
G (nx.DiGraph): The directed graph representing the vessels and connections. 有向图, 节点为容器和注射泵, 边为流体管道, A→B边的属性为管道接A端的阀门位置 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. from_vessel (str): The name of the vessel to transfer from.
@@ -164,50 +192,64 @@ def generate_pump_protocol_with_rinsing(
solid (bool, optional): Indicates if the transfer involves a solid (default is False). 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时的流速 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). 泵骨架中转移流速(若不指定,默认与注入流速相同) transfer_flowrate (float, optional): The flow rate for the transfer action (default is 0.5). 泵骨架中转移流速(若不指定,默认与注入流速相同)
Returns: Returns:
list[dict]: A sequence of pump actions to be executed for the transfer and rinsing process. 泵操作的动作序列. list[dict]: A sequence of pump actions to be executed for the transfer and rinsing process. 泵操作的动作序列.
Raises: Raises:
AssertionError: If the number of rinsing solvents does not match the number of rinsing repeats. AssertionError: If the number of rinsing solvents does not match the number of rinsing repeats.
Examples: Examples:
pump_protocol = generate_pump_protocol_with_rinsing(G, "vessel_A", "vessel_B", 0.1, rinsing_solvent="water") pump_protocol = generate_pump_protocol_with_rinsing(G, "vessel_A", "vessel_B", 0.1, rinsing_solvent="water")
""" """
air_vessel = "flask_air" air_vessel = "flask_air"
waste_vessel = f"waste_workup" waste_vessel = f"waste_workup"
shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel) shortest_path = nx.shortest_path(G, source=from_vessel, target=to_vessel)
pump_backbone = shortest_path[1: -1] pump_backbone = shortest_path[1: -1]
nodes = G.nodes(data=True) nodes = G.nodes(data=True)
min_transfer_volume = float(min([nodes[pump]["max_volume"] for pump in pump_backbone]))
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: if time != 0:
flowrate = transfer_flowrate = volume / time flowrate = transfer_flowrate = volume / time
pump_action_sequence = generate_pump_protocol(G, from_vessel, to_vessel, float(volume), flowrate, transfer_flowrate) pump_action_sequence = generate_pump_protocol(G, from_vessel, to_vessel, float(volume), flowrate, transfer_flowrate)
if rinsing_solvent != "air": if rinsing_solvent != "air" and rinsing_solvent != "":
if "," in rinsing_solvent: if "," in rinsing_solvent:
rinsing_solvents = rinsing_solvent.split(",") 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: else:
rinsing_solvents = [rinsing_solvent] * rinsing_repeats rinsing_solvents = [rinsing_solvent] * rinsing_repeats
for rinsing_solvent in rinsing_solvents: for rinsing_solvent in rinsing_solvents:
solvent_vessel = f"flask_{rinsing_solvent}" solvent_vessel = f"flask_{rinsing_solvent}"
# 清洗泵 # 清洗泵
pump_action_sequence.extend( pump_action_sequence.extend(
generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate, transfer_flowrate) + generate_pump_protocol(G, solvent_vessel, pump_backbone[0], min_transfer_volume, flowrate,
generate_pump_protocol(G, pump_backbone[0], pump_backbone[-1], min_transfer_volume, flowrate, transfer_flowrate) + transfer_flowrate) +
generate_pump_protocol(G, pump_backbone[-1], waste_vessel, 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]: 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(
pump_action_sequence.extend(generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate)) generate_pump_protocol(G, solvent_vessel, from_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(
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, waste_vessel, rinsing_volume, flowrate, transfer_flowrate)) generate_pump_protocol(G, solvent_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate))
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, from_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2) pump_action_sequence.extend(
pump_action_sequence.extend(generate_pump_protocol(G, air_vessel, to_vessel, rinsing_volume, flowrate, transfer_flowrate) * 2) 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 return pump_action_sequence
# End Protocols # End Protocols

View File

@@ -0,0 +1,312 @@
from typing import List, Dict, Any
import networkx as nx
from .pump_protocol import generate_pump_protocol
def get_vessel_liquid_volume(G: nx.DiGraph, vessel: str) -> float:
"""获取容器中的液体体积"""
if vessel not in G.nodes():
return 0.0
vessel_data = G.nodes[vessel].get('data', {})
liquids = vessel_data.get('liquid', [])
total_volume = 0.0
for liquid in liquids:
if isinstance(liquid, dict):
# 支持两种格式:新格式 (name, volume) 和旧格式 (liquid_type, liquid_volume)
volume = liquid.get('volume') or liquid.get('liquid_volume', 0.0)
total_volume += volume
return total_volume
def find_column_device(G: nx.DiGraph, column: str) -> str:
"""查找柱层析设备"""
# 首先检查是否有虚拟柱设备
column_nodes = [node for node in G.nodes()
if (G.nodes[node].get('class') or '') == 'virtual_column']
if column_nodes:
return column_nodes[0]
# 如果没有虚拟柱设备,抛出异常
raise ValueError(f"系统中未找到柱层析设备。请确保配置了 virtual_column 设备")
def find_column_vessel(G: nx.DiGraph, column: str) -> str:
"""查找柱容器"""
# 直接使用 column 参数作为容器名称
if column in G.nodes():
return column
# 尝试常见的柱容器命名规则
possible_names = [
f"column_{column}",
f"{column}_column",
f"vessel_{column}",
f"{column}_vessel",
"column_vessel",
"chromatography_column",
"silica_column",
"preparative_column"
]
for vessel_name in possible_names:
if vessel_name in G.nodes():
return vessel_name
raise ValueError(f"未找到柱容器 '{column}'。尝试了以下名称: {[column] + possible_names}")
def find_eluting_solvent_vessel(G: nx.DiGraph, eluting_solvent: str) -> str:
"""查找洗脱溶剂容器"""
if not eluting_solvent:
return ""
# 按照命名规则查找溶剂瓶
solvent_vessel_id = f"flask_{eluting_solvent}"
if solvent_vessel_id in G.nodes():
return solvent_vessel_id
# 如果直接匹配失败,尝试模糊匹配
for node in G.nodes():
if node.startswith('flask_') and eluting_solvent.lower() in node.lower():
return node
# 如果还是找不到,列出所有可用的溶剂瓶
available_flasks = [node for node in G.nodes()
if node.startswith('flask_')
and G.nodes[node].get('type') == 'container']
raise ValueError(f"找不到洗脱溶剂 '{eluting_solvent}' 对应的溶剂瓶。可用溶剂瓶: {available_flasks}")
def generate_run_column_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
column: str
) -> List[Dict[str, Any]]:
"""
生成柱层析分离的协议序列
Args:
G: 有向图,节点为设备和容器,边为流体管道
from_vessel: 源容器的名称,即样品起始所在的容器
to_vessel: 目标容器的名称,分离后的样品要到达的容器
column: 所使用的柱子的名称
Returns:
List[Dict[str, Any]]: 柱层析分离操作的动作序列
"""
action_sequence = []
print(f"RUN_COLUMN: 开始生成柱层析协议")
print(f" - 源容器: {from_vessel}")
print(f" - 目标容器: {to_vessel}")
print(f" - 柱子: {column}")
# 验证源容器和目标容器存在
if from_vessel not in G.nodes():
raise ValueError(f"源容器 '{from_vessel}' 不存在于系统中")
if to_vessel not in G.nodes():
raise ValueError(f"目标容器 '{to_vessel}' 不存在于系统中")
# 查找柱层析设备
column_device_id = None
column_nodes = [node for node in G.nodes()
if (G.nodes[node].get('class') or '') == 'virtual_column']
if column_nodes:
column_device_id = column_nodes[0]
print(f"RUN_COLUMN: 找到柱层析设备: {column_device_id}")
else:
print(f"RUN_COLUMN: 警告 - 未找到柱层析设备")
# 获取源容器中的液体体积
source_volume = get_vessel_liquid_volume(G, from_vessel)
print(f"RUN_COLUMN: 源容器 {from_vessel} 中有 {source_volume} mL 液体")
# === 第一步:样品转移到柱子(如果柱子是容器) ===
if column in G.nodes() and G.nodes[column].get('type') == 'container':
print(f"RUN_COLUMN: 样品转移 - {source_volume} mL 从 {from_vessel}{column}")
try:
sample_transfer_actions = generate_pump_protocol(
G=G,
from_vessel=from_vessel,
to_vessel=column,
volume=source_volume if source_volume > 0 else 100.0,
flowrate=2.0
)
action_sequence.extend(sample_transfer_actions)
except Exception as e:
print(f"RUN_COLUMN: 样品转移失败: {str(e)}")
# === 第二步:使用柱层析设备执行分离 ===
if column_device_id:
print(f"RUN_COLUMN: 使用柱层析设备执行分离")
column_separation_action = {
"device_id": column_device_id,
"action_name": "run_column",
"action_kwargs": {
"from_vessel": from_vessel,
"to_vessel": to_vessel,
"column": column
}
}
action_sequence.append(column_separation_action)
# 等待柱层析设备完成分离
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 60}
})
# === 第三步:从柱子转移到目标容器(如果需要) ===
if column in G.nodes() and column != to_vessel:
print(f"RUN_COLUMN: 产物转移 - 从 {column}{to_vessel}")
try:
product_transfer_actions = generate_pump_protocol(
G=G,
from_vessel=column,
to_vessel=to_vessel,
volume=source_volume * 0.8 if source_volume > 0 else 80.0, # 假设有一些损失
flowrate=1.5
)
action_sequence.extend(product_transfer_actions)
except Exception as e:
print(f"RUN_COLUMN: 产物转移失败: {str(e)}")
print(f"RUN_COLUMN: 生成了 {len(action_sequence)} 个动作")
return action_sequence
# 便捷函数:常用柱层析方案
def generate_flash_column_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
column_material: str = "silica_gel",
mobile_phase: str = "ethyl_acetate",
mobile_phase_volume: float = 100.0
) -> List[Dict[str, Any]]:
"""快速柱层析:高流速分离"""
return generate_run_column_protocol(
G, from_vessel, to_vessel, column_material,
mobile_phase, mobile_phase_volume, 1, "", 0.0, 3.0
)
def generate_preparative_column_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
column_material: str = "silica_gel",
equilibration_solvent: str = "hexane",
eluting_solvent: str = "ethyl_acetate",
eluting_volume: float = 50.0,
eluting_repeats: int = 3
) -> List[Dict[str, Any]]:
"""制备柱层析:带平衡和多次洗脱"""
return generate_run_column_protocol(
G, from_vessel, to_vessel, column_material,
eluting_solvent, eluting_volume, eluting_repeats,
equilibration_solvent, 30.0, 1.5
)
def generate_gradient_column_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
column_material: str = "silica_gel",
gradient_solvents: List[str] = None,
gradient_volumes: List[float] = None
) -> List[Dict[str, Any]]:
"""梯度洗脱柱层析:多种溶剂系统"""
if gradient_solvents is None:
gradient_solvents = ["hexane", "ethyl_acetate", "methanol"]
if gradient_volumes is None:
gradient_volumes = [50.0, 50.0, 30.0]
action_sequence = []
# 每种溶剂单独执行一次柱层析
for i, (solvent, volume) in enumerate(zip(gradient_solvents, gradient_volumes)):
print(f"RUN_COLUMN: 梯度洗脱第 {i+1}/{len(gradient_solvents)} 步: {volume} mL {solvent}")
# 第一步使用源容器,后续步骤使用柱子作为源
step_from_vessel = from_vessel if i == 0 else column_material
# 最后一步使用目标容器,其他步骤使用柱子作为目标
step_to_vessel = to_vessel if i == len(gradient_solvents) - 1 else column_material
step_actions = generate_run_column_protocol(
G, step_from_vessel, step_to_vessel, column_material,
solvent, volume, 1, "", 0.0, 1.0
)
action_sequence.extend(step_actions)
# 在梯度步骤之间加入等待时间
if i < len(gradient_solvents) - 1:
action_sequence.append({
"action_name": "wait",
"action_kwargs": {"time": 20}
})
return action_sequence
def generate_reverse_phase_column_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
column_material: str = "C18",
aqueous_phase: str = "water",
organic_phase: str = "methanol",
gradient_ratio: float = 0.5
) -> List[Dict[str, Any]]:
"""反相柱层析C18柱水-有机相梯度"""
# 先用水相平衡
equilibration_volume = 20.0
# 然后用有机相洗脱
eluting_volume = 30.0 * gradient_ratio
return generate_run_column_protocol(
G, from_vessel, to_vessel, column_material,
organic_phase, eluting_volume, 2,
aqueous_phase, equilibration_volume, 0.8
)
def generate_ion_exchange_column_protocol(
G: nx.DiGraph,
from_vessel: str,
to_vessel: str,
column_material: str = "ion_exchange",
buffer_solution: str = "buffer",
salt_solution: str = "NaCl_solution",
salt_volume: float = 40.0
) -> List[Dict[str, Any]]:
"""离子交换柱层析:缓冲液平衡,盐溶液洗脱"""
return generate_run_column_protocol(
G, from_vessel, to_vessel, column_material,
salt_solution, salt_volume, 1,
buffer_solution, 25.0, 0.5
)
# 测试函数
def test_run_column_protocol():
"""测试柱层析协议的示例"""
print("=== RUN COLUMN PROTOCOL 测试 ===")
print("测试完成")
if __name__ == "__main__":
test_run_column_protocol()

View File

@@ -0,0 +1,166 @@
from typing import List, Dict, Any
import networkx as nx
def find_connected_stirrer(G: nx.DiGraph, vessel: str = None) -> str:
"""
查找与指定容器相连的搅拌设备,或查找可用的搅拌设备
"""
# 查找所有搅拌设备节点
stirrer_nodes = [node for node in G.nodes()
if (G.nodes[node].get('class') or '') == 'virtual_stirrer']
if vessel:
# 检查哪个搅拌设备与目标容器相连(机械连接)
for stirrer in stirrer_nodes:
if G.has_edge(stirrer, vessel) or G.has_edge(vessel, stirrer):
return stirrer
# 如果没有指定容器或没有直接连接,返回第一个可用的搅拌设备
if stirrer_nodes:
return stirrer_nodes[0]
raise ValueError("系统中未找到可用的搅拌设备")
def generate_stir_protocol(
G: nx.DiGraph,
stir_time: float,
stir_speed: float,
settling_time: float
) -> List[Dict[str, Any]]:
"""
生成搅拌操作的协议序列 - 定时搅拌 + 沉降
"""
action_sequence = []
print(f"STIR: 开始生成搅拌协议")
print(f" - 搅拌时间: {stir_time}")
print(f" - 搅拌速度: {stir_speed} RPM")
print(f" - 沉降时间: {settling_time}")
# 查找搅拌设备
try:
stirrer_id = find_connected_stirrer(G)
print(f"STIR: 找到搅拌设备: {stirrer_id}")
except ValueError as e:
raise ValueError(f"无法找到搅拌设备: {str(e)}")
# 执行搅拌操作
stir_action = {
"device_id": stirrer_id,
"action_name": "stir",
"action_kwargs": {
"stir_time": stir_time,
"stir_speed": stir_speed,
"settling_time": settling_time
}
}
action_sequence.append(stir_action)
print(f"STIR: 生成了 {len(action_sequence)} 个动作")
return action_sequence
def generate_start_stir_protocol(
G: nx.DiGraph,
vessel: str,
stir_speed: float,
purpose: str
) -> List[Dict[str, Any]]:
"""
生成开始搅拌操作的协议序列 - 持续搅拌
"""
action_sequence = []
print(f"START_STIR: 开始生成启动搅拌协议")
print(f" - 容器: {vessel}")
print(f" - 搅拌速度: {stir_speed} RPM")
print(f" - 目的: {purpose}")
# 验证容器存在
if vessel not in G.nodes():
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
# 查找搅拌设备
try:
stirrer_id = find_connected_stirrer(G, vessel)
print(f"START_STIR: 找到搅拌设备: {stirrer_id}")
except ValueError as e:
raise ValueError(f"无法找到搅拌设备: {str(e)}")
# 执行开始搅拌操作
start_stir_action = {
"device_id": stirrer_id,
"action_name": "start_stir",
"action_kwargs": {
"vessel": vessel,
"stir_speed": stir_speed,
"purpose": purpose
}
}
action_sequence.append(start_stir_action)
print(f"START_STIR: 生成了 {len(action_sequence)} 个动作")
return action_sequence
def generate_stop_stir_protocol(
G: nx.DiGraph,
vessel: str
) -> List[Dict[str, Any]]:
"""
生成停止搅拌操作的协议序列
"""
action_sequence = []
print(f"STOP_STIR: 开始生成停止搅拌协议")
print(f" - 容器: {vessel}")
# 验证容器存在
if vessel not in G.nodes():
raise ValueError(f"容器 '{vessel}' 不存在于系统中")
# 查找搅拌设备
try:
stirrer_id = find_connected_stirrer(G, vessel)
print(f"STOP_STIR: 找到搅拌设备: {stirrer_id}")
except ValueError as e:
raise ValueError(f"无法找到搅拌设备: {str(e)}")
# 执行停止搅拌操作
stop_stir_action = {
"device_id": stirrer_id,
"action_name": "stop_stir",
"action_kwargs": {
"vessel": vessel
}
}
action_sequence.append(stop_stir_action)
print(f"STOP_STIR: 生成了 {len(action_sequence)} 个动作")
return action_sequence
# 便捷函数
def generate_fast_stir_protocol(
G: nx.DiGraph,
time: float = 300.0,
speed: float = 800.0,
settling: float = 60.0
) -> List[Dict[str, Any]]:
"""快速搅拌的便捷函数"""
return generate_stir_protocol(G, time, speed, settling)
def generate_gentle_stir_protocol(
G: nx.DiGraph,
time: float = 600.0,
speed: float = 200.0,
settling: float = 120.0
) -> List[Dict[str, Any]]:
"""温和搅拌的便捷函数"""
return generate_stir_protocol(G, time, speed, settling)

View File

@@ -0,0 +1,79 @@
from typing import List, Dict, Any
import networkx as nx
def generate_transfer_protocol(
G: nx.DiGraph,
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
) -> List[Dict[str, Any]]:
"""
生成液体转移操作的协议序列
Args:
G: 有向图,节点为设备和容器
from_vessel: 源容器
to_vessel: 目标容器
volume: 转移体积 (mL)
amount: 数量描述 (可选)
time: 转移时间 (秒,可选)
viscous: 是否为粘性液体
rinsing_solvent: 冲洗溶剂 (可选)
rinsing_volume: 冲洗体积 (mL可选)
rinsing_repeats: 冲洗重复次数
solid: 是否涉及固体
Returns:
List[Dict[str, Any]]: 转移操作的动作序列
Raises:
ValueError: 当找不到合适的转移设备时抛出异常
Examples:
transfer_protocol = generate_transfer_protocol(G, "flask_1", "reactor", 10.0)
"""
action_sequence = []
# 查找虚拟转移泵设备用于液体转移 - 修复:应该查找 virtual_transfer_pump
pump_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_transfer_pump']
if not pump_nodes:
raise ValueError("没有找到可用的转移泵设备进行液体转移")
# 使用第一个可用的泵
pump_id = pump_nodes[0]
# 验证容器是否存在
if from_vessel not in G.nodes():
raise ValueError(f"源容器 {from_vessel} 不存在于图中")
if to_vessel not in G.nodes():
raise ValueError(f"目标容器 {to_vessel} 不存在于图中")
# 执行液体转移操作 - 参数完全匹配Transfer.action
action_sequence.append({
"device_id": pump_id,
"action_name": "transfer",
"action_kwargs": {
"from_vessel": from_vessel,
"to_vessel": to_vessel,
"volume": volume,
"amount": amount,
"time": time,
"viscous": viscous,
"rinsing_solvent": rinsing_solvent,
"rinsing_volume": rinsing_volume,
"rinsing_repeats": rinsing_repeats,
"solid": solid
}
})
return action_sequence

View File

@@ -0,0 +1,216 @@
from typing import List, Dict, Any
import networkx as nx
def generate_wash_solid_protocol(
G: nx.DiGraph,
vessel: str,
solvent: str,
volume: float,
filtrate_vessel: str = "",
temp: float = 25.0,
stir: bool = False,
stir_speed: float = 0.0,
time: float = 0.0,
repeats: int = 1
) -> List[Dict[str, Any]]:
"""
生成固体清洗的协议序列
Args:
G: 有向图,节点为设备和容器
vessel: 装有固体物质的容器名称
solvent: 用于清洗固体的溶剂名称
volume: 清洗溶剂的体积
filtrate_vessel: 滤液要收集到的容器名称,可选参数
temp: 清洗时的温度,可选参数
stir: 是否在清洗过程中搅拌,默认为 False
stir_speed: 搅拌速度,可选参数
time: 清洗的时间,可选参数
repeats: 清洗操作的重复次数,默认为 1
Returns:
List[Dict[str, Any]]: 固体清洗操作的动作序列
Raises:
ValueError: 当找不到必要的设备时抛出异常
Examples:
wash_solid_protocol = generate_wash_solid_protocol(
G, "reactor", "ethanol", 100.0, "waste_flask", 60.0, True, 300.0, 600.0, 3
)
"""
action_sequence = []
# 验证容器是否存在
if vessel not in G.nodes():
raise ValueError(f"固体容器 {vessel} 不存在于图中")
if filtrate_vessel and filtrate_vessel not in G.nodes():
raise ValueError(f"滤液容器 {filtrate_vessel} 不存在于图中")
# 查找转移泵设备(用于添加溶剂和转移滤液)
pump_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_transfer_pump']
if not pump_nodes:
raise ValueError("没有找到可用的转移泵设备")
pump_id = pump_nodes[0]
# 查找加热设备(如果需要加热)
heatchill_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_heatchill']
heatchill_id = heatchill_nodes[0] if heatchill_nodes else None
# 查找搅拌设备(如果需要搅拌)
stirrer_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_stirrer']
stirrer_id = stirrer_nodes[0] if stirrer_nodes else None
# 查找过滤设备(用于分离固体和滤液)
filter_nodes = [node for node in G.nodes()
if G.nodes[node].get('class') == 'virtual_filter']
filter_id = filter_nodes[0] if filter_nodes else None
# 查找溶剂容器
solvent_vessel = f"flask_{solvent}"
if solvent_vessel not in G.nodes():
# 如果没有找到特定溶剂容器,查找可用的源容器
available_vessels = [node for node in G.nodes()
if node.startswith('flask_') and
G.nodes[node].get('type') == 'container']
if available_vessels:
solvent_vessel = available_vessels[0]
else:
raise ValueError(f"没有找到溶剂容器 {solvent}")
# 如果没有指定滤液容器,使用废液容器
if not filtrate_vessel:
waste_vessels = [node for node in G.nodes()
if 'waste' in node.lower() and
G.nodes[node].get('type') == 'container']
filtrate_vessel = waste_vessels[0] if waste_vessels else "waste_flask"
# 重复清洗操作
for repeat in range(repeats):
repeat_num = repeat + 1
# 步骤1如果需要加热先设置温度
if temp > 25.0 and heatchill_id:
action_sequence.append({
"device_id": heatchill_id,
"action_name": "heat_chill_start",
"action_kwargs": {
"vessel": vessel,
"temp": temp,
"purpose": f"固体清洗 - 第 {repeat_num}"
}
})
# 步骤2添加清洗溶剂到固体容器
action_sequence.append({
"device_id": pump_id,
"action_name": "transfer",
"action_kwargs": {
"from_vessel": solvent_vessel,
"to_vessel": vessel,
"volume": volume,
"amount": f"清洗溶剂 {solvent} - 第 {repeat_num}",
"time": 0.0,
"viscous": False,
"rinsing_solvent": "",
"rinsing_volume": 0.0,
"rinsing_repeats": 0,
"solid": False
}
})
# 步骤3如果需要搅拌开始搅拌
if stir and stir_speed > 0 and stirrer_id:
if time > 0:
# 定时搅拌
action_sequence.append({
"device_id": stirrer_id,
"action_name": "stir",
"action_kwargs": {
"stir_time": time,
"stir_speed": stir_speed,
"settling_time": 30.0 # 搅拌后静置30秒
}
})
else:
# 开始搅拌(需要手动停止)
action_sequence.append({
"device_id": stirrer_id,
"action_name": "start_stir",
"action_kwargs": {
"vessel": vessel,
"stir_speed": stir_speed,
"purpose": f"固体清洗搅拌 - 第 {repeat_num}"
}
})
# 步骤4如果指定了清洗时间但没有搅拌等待清洗时间
if time > 0 and (not stir or stir_speed == 0):
# 这里可以添加等待操作,暂时跳过
pass
# 步骤5如果有搅拌且没有定时停止搅拌
if stir and stir_speed > 0 and time == 0 and stirrer_id:
action_sequence.append({
"device_id": stirrer_id,
"action_name": "stop_stir",
"action_kwargs": {
"vessel": vessel
}
})
# 步骤6过滤分离固体和滤液
if filter_id:
action_sequence.append({
"device_id": filter_id,
"action_name": "filter_sample",
"action_kwargs": {
"vessel": vessel,
"filtrate_vessel": filtrate_vessel,
"stir": False,
"stir_speed": 0.0,
"temp": temp,
"continue_heatchill": temp > 25.0,
"volume": volume
}
})
else:
# 没有专门的过滤设备,使用转移泵模拟过滤过程
# 将滤液转移到滤液容器
action_sequence.append({
"device_id": pump_id,
"action_name": "transfer",
"action_kwargs": {
"from_vessel": vessel,
"to_vessel": filtrate_vessel,
"volume": volume,
"amount": f"转移滤液 - 第 {repeat_num} 次清洗",
"time": 0.0,
"viscous": False,
"rinsing_solvent": "",
"rinsing_volume": 0.0,
"rinsing_repeats": 0,
"solid": False
}
})
# 步骤7如果加热了停止加热在最后一次清洗后
if temp > 25.0 and heatchill_id and repeat_num == repeats:
action_sequence.append({
"device_id": heatchill_id,
"action_name": "heat_chill_stop",
"action_kwargs": {
"vessel": vessel
}
})
return action_sequence

View File

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

View File

@@ -9,7 +9,7 @@ Panels:
- /MotionPlanning1/Scene Robot1 - /MotionPlanning1/Scene Robot1
- /MotionPlanning1/Planning Request1 - /MotionPlanning1/Planning Request1
Splitter Ratio: 0.5016146302223206 Splitter Ratio: 0.5016146302223206
Tree Height: 575 Tree Height: 1112
- Class: rviz_common/Selection - Class: rviz_common/Selection
Name: Selection Name: Selection
- Class: rviz_common/Tool Properties - Class: rviz_common/Tool Properties
@@ -571,21 +571,21 @@ Visualization Manager:
Saved: ~ Saved: ~
Window Geometry: Window Geometry:
Displays: Displays:
collapsed: true collapsed: false
Height: 1061 Height: 2032
Hide Left Dock: true Hide Left Dock: false
Hide Right Dock: true Hide Right Dock: true
MotionPlanning: MotionPlanning:
collapsed: true collapsed: true
MotionPlanning - Trajectory Slider: MotionPlanning - Trajectory Slider:
collapsed: false collapsed: false
QMainWindow State: 000000ff00000000fd0000000400000000000003a3000003f2fc020000000bfb0000001200530065006c0065006300740069006f006e00000001e10000009b0000005d00fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c00610079007300000000170000027b000000ca00fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb000000280020002d0020005400720061006a006500630074006f0072007900200053006c00690064006500720000000000ffffffff0000000000000000fb00000044004d006f00740069006f006e0050006c0061006e006e0069006e00670020002d0020005400720061006a006500630074006f0072007900200053006c00690064006500720000000000ffffffff0000004300fffffffb0000001c004d006f00740069006f006e0050006c0061006e006e0069006e00670000000296000001730000013e00ffffff000000010000010f00000387fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073000000003b00000387000000a100fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000004420000003efc0100000002fb0000000800540069006d00650100000000000004420000000000000000fb0000000800540069006d0065010000000000000450000000000000000000000784000003f200000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730000000000ffffffff0000000000000000 QMainWindow State: 000000ff00000000fd0000000400000000000003a30000079bfc020000000bfb0000001200530065006c0065006300740069006f006e00000001e10000009b000000b000fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c0061007900730100000027000004c60000018200fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb000000280020002d0020005400720061006a006500630074006f0072007900200053006c00690064006500720000000000ffffffff0000000000000000fb00000044004d006f00740069006f006e0050006c0061006e006e0069006e00670020002d0020005400720061006a006500630074006f0072007900200053006c00690064006500720000000000ffffffff0000007a00fffffffb0000001c004d006f00740069006f006e0050006c0061006e006e0069006e006701000004f9000002c9000002b800ffffff000000010000010f00000387fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073000000003b000003870000013200fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000004420000003efc0100000002fb0000000800540069006d00650100000000000004420000000000000000fb0000000800540069006d0065010000000000000450000000000000000000000bc50000079b00000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730000000000ffffffff0000000000000000
Selection: Selection:
collapsed: false collapsed: false
Tool Properties: Tool Properties:
collapsed: false collapsed: false
Views: Views:
collapsed: true collapsed: true
Width: 1924 Width: 3956
X: 140 X: 140
Y: 54 Y: 54

View File

@@ -10,6 +10,7 @@ from pylabrobot.resources import (
Container, Container,
Coordinate, Coordinate,
) )
import copy
from unilabos_msgs.msg import Resource from unilabos_msgs.msg import Resource
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker # type: ignore from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker # type: ignore
@@ -61,7 +62,7 @@ class LiquidHandlerBiomek:
'LocalPattern': True, 'LocalPattern': True,
'Operation': 'Aspirate', 'Operation': 'Aspirate',
'OverrideHeight': False, 'OverrideHeight': False,
'Pattern': (True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True), 'Pattern': (True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True),
'Prototype': 'MC P300 High', 'Prototype': 'MC P300 High',
'ReferencedPattern': '', 'ReferencedPattern': '',
'RowsFirst': False, 'RowsFirst': False,
@@ -450,8 +451,8 @@ class LiquidHandlerBiomek:
""" """
items = [] items = []
asp_params = self.aspirate_techniques.get(aspirate_techniques, {}) asp_params = copy.deepcopy(self.aspirate_techniques[aspirate_techniques])
dis_params = self.dispense_techniques.get(dispense_techniques, {}) dis_params = copy.deepcopy(self.dispense_techniques[dispense_techniques])
asp_params['Position'] = source asp_params['Position'] = source
dis_params['Position'] = target dis_params['Position'] = target
@@ -564,7 +565,7 @@ class LiquidHandlerBiomek:
if __name__ == "__main__": if __name__ == "__main__":
print("=== Biomek完整流程测试 ===") print("=== Biomek完整流程测试 ===")
print("包含: 仪器设置 + 完整实验步骤") print("包含: 仪器设置 + 完整实验步骤")
@@ -968,15 +969,6 @@ if __name__ == "__main__":
"liquid_volume": [], "liquid_volume": [],
"liquid_input_wells": [] "liquid_input_wells": []
}, },
{
"id": "working plate on P12",
"parent": "deck",
"slot_on_deck": "P12",
"class_name": "BCDeep96Round",
"liquid_type": [],
"liquid_volume": [],
"liquid_input_wells": []
},
{ {
"id": "working plate on P13", "id": "working plate on P13",
"parent": "deck", "parent": "deck",

View File

@@ -15,6 +15,7 @@ from typing import List, Sequence, Optional, Union, Literal
import json import json
import pathlib import pathlib
from typing import Sequence, Optional, List, Union, Literal from typing import Sequence, Optional, List, Union, Literal
import copy
@@ -67,7 +68,7 @@ class LiquidHandlerBiomek:
'LocalPattern': True, 'LocalPattern': True,
'Operation': 'Aspirate', 'Operation': 'Aspirate',
'OverrideHeight': False, 'OverrideHeight': False,
'Pattern': (True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True), 'Pattern': (True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True, True),
'Prototype': 'MC P300 High', 'Prototype': 'MC P300 High',
'ReferencedPattern': '', 'ReferencedPattern': '',
'RowsFirst': False, 'RowsFirst': False,
@@ -351,8 +352,9 @@ class LiquidHandlerBiomek:
""" """
items = [] items = []
asp_params = self.aspirate_techniques.get(aspirate_techniques, {}) asp_params = copy.deepcopy(self.aspirate_techniques[aspirate_techniques])
dis_params = self.dispense_techniques.get(dispense_techniques, {}) dis_params = copy.deepcopy(self.dispense_techniques[dispense_techniques])
asp_params['Position'] = source asp_params['Position'] = source
dis_params['Position'] = target dis_params['Position'] = target
@@ -403,7 +405,9 @@ class LiquidHandlerBiomek:
transfer_params["Solvent"] = 'Water' transfer_params["Solvent"] = 'Water'
transfer_params["TipLocation"] = tip_rack transfer_params["TipLocation"] = tip_rack
tmp={'transfer': transfer_params} tmp={'transfer': transfer_params}
self.temp_protocol["steps"].append(tmp) self.temp_protocol["steps"].append(tmp)
return return
@@ -872,15 +876,6 @@ if __name__ == "__main__":
"liquid_volume": [], "liquid_volume": [],
"liquid_input_wells": [] "liquid_input_wells": []
}, },
{
"id": "working plate on P12",
"parent": "deck",
"slot_on_deck": "P12",
"class_name": "BCDeep96Round",
"liquid_type": [],
"liquid_volume": [],
"liquid_input_wells": []
},
{ {
"id": "working plate on P13", "id": "working plate on P13",
"parent": "deck", "parent": "deck",
@@ -968,6 +963,7 @@ if __name__ == "__main__":
print(f"步骤 {step['step_number']}: {description}") print(f"步骤 {step['step_number']}: {description}")
if operation == 'transfer': if operation == 'transfer':
handler.transfer_biomek( handler.transfer_biomek(
source=parameters['source'], source=parameters['source'],
target=parameters['target'], target=parameters['target'],

View File

View File

@@ -0,0 +1,177 @@
import time
import threading
class MockChiller:
def __init__(self, port: str = "MOCK"):
self.port = port
self._current_temperature: float = 25.0 # 室温开始
self._target_temperature: float = 25.0
self._status: str = "Idle"
self._is_cooling: bool = False
self._is_heating: bool = False
self._vessel = "Unknown"
self._purpose = "Unknown"
# 模拟温度变化的线程
self._temperature_thread = None
self._running = True
self._temperature_thread = threading.Thread(target=self._temperature_control_loop)
self._temperature_thread.daemon = True
self._temperature_thread.start()
@property
def current_temperature(self) -> float:
"""当前温度 - 会被自动识别的设备属性"""
return self._current_temperature
@property
def target_temperature(self) -> float:
"""目标温度"""
return self._target_temperature
@property
def status(self) -> str:
"""设备状态 - 会被自动识别的设备属性"""
return self._status
@property
def is_cooling(self) -> bool:
"""是否正在冷却"""
return self._is_cooling
@property
def is_heating(self) -> bool:
"""是否正在加热"""
return self._is_heating
@property
def vessel(self) -> str:
"""当前操作的容器名称"""
return self._vessel
@property
def purpose(self) -> str:
"""当前操作目的"""
return self._purpose
def heat_chill_start(self, vessel: str, temp: float, purpose: str):
"""设置目标温度并记录容器和目的"""
self._vessel = str(vessel)
self._purpose = str(purpose)
self._target_temperature = float(temp)
diff = self._target_temperature - self._current_temperature
if abs(diff) < 0.1:
self._status = "At Target Temperature"
self._is_cooling = False
self._is_heating = False
elif diff < 0:
self._status = "Cooling"
self._is_cooling = True
self._is_heating = False
else:
self._status = "Heating"
self._is_heating = True
self._is_cooling = False
self._start_temperature_control()
return True
def heat_chill_stop(self, vessel: str):
"""停止加热/制冷"""
if vessel != self._vessel:
return {"success": False, "status": f"Wrong vessel: expected {self._vessel}, got {vessel}"}
# 停止温度控制线程,锁定当前温度
self._stop_temperature_control()
# 更新状态
self._status = "Stopped"
self._is_cooling = False
self._is_heating = False
# 重新启动线程但保持温度
self._running = True
self._temperature_thread = threading.Thread(target=self._temperature_control_loop)
self._temperature_thread.daemon = True
self._temperature_thread.start()
return {"success": True, "status": self._status}
def _start_temperature_control(self):
"""启动温度控制线程"""
self._running = True
if self._temperature_thread is None or not self._temperature_thread.is_alive():
self._temperature_thread = threading.Thread(target=self._temperature_control_loop)
self._temperature_thread.daemon = True
self._temperature_thread.start()
def _stop_temperature_control(self):
"""停止温度控制"""
self._running = False
if self._temperature_thread:
self._temperature_thread.join(timeout=1.0)
def _temperature_control_loop(self):
"""温度控制循环 - 模拟真实冷却器的温度变化"""
while self._running:
# 如果状态是 Stopped不改变温度
if self._status == "Stopped":
time.sleep(1.0)
continue
temp_diff = self._target_temperature - self._current_temperature
if abs(temp_diff) < 0.1:
self._status = "At Target Temperature"
self._is_cooling = False
self._is_heating = False
elif temp_diff < 0:
self._status = "Cooling"
self._is_cooling = True
self._is_heating = False
self._current_temperature -= 0.5
else:
self._status = "Heating"
self._is_heating = True
self._is_cooling = False
self._current_temperature += 0.3
time.sleep(1.0)
def emergency_stop(self):
"""紧急停止"""
self._status = "Emergency Stop"
self._stop_temperature_control()
self._is_cooling = False
self._is_heating = False
def get_status_info(self) -> dict:
"""获取完整状态信息"""
return {
"current_temperature": self._current_temperature,
"target_temperature": self._target_temperature,
"status": self._status,
"is_cooling": self._is_cooling,
"is_heating": self._is_heating,
"vessel": self._vessel,
"purpose": self._purpose,
}
# 用于测试的主函数
if __name__ == "__main__":
chiller = MockChiller()
# 测试基本功能
print("启动冷却器测试...")
print(f"初始状态: {chiller.get_status_info()}")
# 模拟运行10秒
for i in range(10):
time.sleep(1)
print(f"{i+1}秒: 当前温度={chiller.current_temperature:.1f}°C, 状态={chiller.status}")
chiller.emergency_stop()
print("测试完成")

View File

@@ -0,0 +1,235 @@
import time
import threading
class MockFilter:
def __init__(self, port: str = "MOCK"):
# 基本参数初始化
self.port = port
self._status: str = "Idle"
self._is_filtering: bool = False
# 过滤性能参数
self._flow_rate: float = 1.0 # 流速(L/min)
self._pressure_drop: float = 0.0 # 压降(Pa)
self._filter_life: float = 100.0 # 滤芯寿命(%)
# 过滤操作参数
self._vessel: str = "" # 源容器
self._filtrate_vessel: str = "" # 目标容器
self._stir: bool = False # 是否搅拌
self._stir_speed: float = 0.0 # 搅拌速度
self._temperature: float = 25.0 # 温度(℃)
self._continue_heatchill: bool = False # 是否继续加热/制冷
self._target_volume: float = 0.0 # 目标过滤体积(L)
self._filtered_volume: float = 0.0 # 已过滤体积(L)
self._progress: float = 0.0 # 过滤进度(%)
# 线程控制
self._filter_thread = None
self._running = False
@property
def status(self) -> str:
return self._status
@property
def is_filtering(self) -> bool:
return self._is_filtering
@property
def flow_rate(self) -> float:
return self._flow_rate
@property
def pressure_drop(self) -> float:
return self._pressure_drop
@property
def filter_life(self) -> float:
return self._filter_life
# 新增 property
@property
def vessel(self) -> str:
return self._vessel
@property
def filtrate_vessel(self) -> str:
return self._filtrate_vessel
@property
def filtered_volume(self) -> float:
return self._filtered_volume
@property
def progress(self) -> float:
return self._progress
@property
def stir(self) -> bool:
return self._stir
@property
def stir_speed(self) -> float:
return self._stir_speed
@property
def temperature(self) -> float:
return self._temperature
@property
def continue_heatchill(self) -> bool:
return self._continue_heatchill
@property
def target_volume(self) -> float:
return self._target_volume
def filter(self, vessel: str, filtrate_vessel: str, stir: bool = False, stir_speed: float = 0.0, temp: float = 25.0, continue_heatchill: bool = False, volume: float = 0.0) -> dict:
"""新的过滤操作"""
# 停止任何正在进行的过滤
if self._is_filtering:
self.stop_filtering()
# 验证参数
if volume <= 0:
return {"success": False, "message": "Target volume must be greater than 0"}
# 设置新的过滤参数
self._vessel = vessel
self._filtrate_vessel = filtrate_vessel
self._stir = stir
self._stir_speed = stir_speed
self._temperature = temp
self._continue_heatchill = continue_heatchill
self._target_volume = volume
# 重置过滤状态
self._filtered_volume = 0.0
self._progress = 0.0
self._status = "Starting Filter"
# 启动过滤过程
self._flow_rate = 1.0 # 设置默认流速
self._start_filter_process()
return {"success": True, "message": "Filter started"}
def stop_filtering(self):
"""停止过滤"""
self._status = "Stopping Filter"
self._stop_filter_process()
self._flow_rate = 0.0
self._is_filtering = False
self._status = "Stopped"
return True
def replace_filter(self):
"""更换滤芯"""
self._filter_life = 100.0
self._status = "Filter Replaced"
return True
def _start_filter_process(self):
"""启动过滤过程线程"""
if not self._running:
self._running = True
self._is_filtering = True
self._filter_thread = threading.Thread(target=self._filter_loop)
self._filter_thread.daemon = True
self._filter_thread.start()
def _stop_filter_process(self):
"""停止过滤过程"""
self._running = False
if self._filter_thread:
self._filter_thread.join(timeout=1.0)
def _filter_loop(self):
"""过滤进程主循环"""
update_interval = 1.0 # 更新间隔(秒)
while self._running and self._is_filtering:
try:
self._status = "Filtering"
# 计算这一秒过滤的体积 (L/min -> L/s)
volume_increment = (self._flow_rate / 60.0) * update_interval
# 更新已过滤体积
self._filtered_volume += volume_increment
# 更新进度 (避免除零错误)
if self._target_volume > 0:
self._progress = min(100.0, (self._filtered_volume / self._target_volume) * 100.0)
# 更新滤芯寿命 (每过滤1L减少0.5%寿命)
self._filter_life = max(0.0, self._filter_life - (volume_increment * 0.5))
# 更新压降 (根据滤芯寿命和流速动态计算)
life_factor = self._filter_life / 100.0 # 将寿命转换为0-1的因子
flow_factor = self._flow_rate / 2.0 # 将流速标准化(假设2L/min是标准流速)
base_pressure = 100.0 # 基础压降
# 压降随滤芯寿命降低而增加,随流速增加而增加
self._pressure_drop = base_pressure * (2 - life_factor) * flow_factor
# 检查是否完成目标体积
if self._target_volume > 0 and self._filtered_volume >= self._target_volume:
self._status = "Completed"
self._progress = 100.0
self.stop_filtering()
break
# 检查滤芯寿命
if self._filter_life <= 10.0:
self._status = "Filter Needs Replacement"
time.sleep(update_interval)
except Exception as e:
print(f"Error in filter loop: {e}")
self.emergency_stop()
break
def emergency_stop(self):
"""紧急停止"""
self._status = "Emergency Stop"
self._stop_filter_process()
self._is_filtering = False
self._flow_rate = 0.0
def get_status_info(self) -> dict:
"""扩展的状态信息"""
return {
"status": self._status,
"is_filtering": self._is_filtering,
"flow_rate": self._flow_rate,
"pressure_drop": self._pressure_drop,
"filter_life": self._filter_life,
"vessel": self._vessel,
"filtrate_vessel": self._filtrate_vessel,
"filtered_volume": self._filtered_volume,
"target_volume": self._target_volume,
"progress": self._progress,
"temperature": self._temperature,
"stir": self._stir,
"stir_speed": self._stir_speed
}
# 用于测试的主函数
if __name__ == "__main__":
filter_device = MockFilter()
# 测试基本功能
print("启动过滤器测试...")
print(f"初始状态: {filter_device.get_status_info()}")
# 模拟运行10秒
for i in range(10):
time.sleep(1)
print(
f"{i+1}秒: "
f"寿命={filter_device.filter_life:.1f}%, 状态={filter_device.status}"
)
filter_device.emergency_stop()
print("测试完成")

View File

@@ -0,0 +1,247 @@
import time
import threading
class MockHeater:
def __init__(self, port: str = "MOCK"):
self.port = port
self._current_temperature: float = 25.0 # 室温开始
self._target_temperature: float = 25.0
self._status: str = "Idle"
self._is_heating: bool = False
self._heating_power: float = 0.0 # 加热功率百分比 0-100
self._max_temperature: float = 300.0 # 最大加热温度
# 新增加的属性
self._vessel: str = "Unknown"
self._purpose: str = "Unknown"
self._stir: bool = False
self._stir_speed: float = 0.0
# 模拟加热过程的线程
self._heating_thread = None
self._running = True
self._heating_thread = threading.Thread(target=self._heating_control_loop)
self._heating_thread.daemon = True
self._heating_thread.start()
@property
def current_temperature(self) -> float:
"""当前温度 - 会被自动识别的设备属性"""
return self._current_temperature
@property
def target_temperature(self) -> float:
"""目标温度"""
return self._target_temperature
@property
def status(self) -> str:
"""设备状态 - 会被自动识别的设备属性"""
return self._status
@property
def is_heating(self) -> bool:
"""是否正在加热"""
return self._is_heating
@property
def heating_power(self) -> float:
"""加热功率百分比"""
return self._heating_power
@property
def max_temperature(self) -> float:
"""最大加热温度"""
return self._max_temperature
@property
def vessel(self) -> str:
"""当前操作的容器名称"""
return self._vessel
@property
def purpose(self) -> str:
"""操作目的"""
return self._purpose
@property
def stir(self) -> bool:
"""是否搅拌"""
return self._stir
@property
def stir_speed(self) -> float:
"""搅拌速度"""
return self._stir_speed
def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> dict:
"""开始加热/制冷过程"""
self._vessel = str(vessel)
self._purpose = str(purpose)
self._target_temperature = float(temp)
diff = self._target_temperature - self._current_temperature
if abs(diff) < 0.1:
self._status = "At Target Temperature"
self._is_heating = False
elif diff > 0:
self._status = "Heating"
self._is_heating = True
else:
self._status = "Cooling Down"
self._is_heating = False
return {"success": True, "status": self._status}
def heat_chill_stop(self, vessel: str) -> dict:
"""停止加热/制冷"""
if vessel != self._vessel:
return {"success": False, "status": f"Wrong vessel: expected {self._vessel}, got {vessel}"}
self._status = "Stopped"
self._is_heating = False
self._heating_power = 0.0
return {"success": True, "status": self._status}
def heat_chill(self, vessel: str, temp: float, time: float,
stir: bool = False, stir_speed: float = 0.0,
purpose: str = "Unknown") -> dict:
"""完整的加热/制冷控制"""
self._vessel = str(vessel)
self._target_temperature = float(temp)
self._purpose = str(purpose)
self._stir = stir
self._stir_speed = stir_speed
diff = self._target_temperature - self._current_temperature
if abs(diff) < 0.1:
self._status = "At Target Temperature"
self._is_heating = False
elif diff > 0:
self._status = "Heating"
self._is_heating = True
else:
self._status = "Cooling Down"
self._is_heating = False
return {"success": True, "status": self._status}
def set_temperature(self, temperature: float):
"""设置目标温度 - 需要在注册表添加的设备动作"""
try:
temperature = float(temperature)
except ValueError:
self._status = "Error: Invalid temperature value"
return False
if temperature > self._max_temperature:
self._status = f"Error: Temperature exceeds maximum ({self._max_temperature}°C)"
return False
self._target_temperature = temperature
self._status = "Setting Temperature"
# 启动加热控制
self._start_heating_control()
return True
def set_heating_power(self, power: float):
"""设置加热功率"""
try:
power = float(power)
except ValueError:
self._status = "Error: Invalid power value"
return False
self._heating_power = max(0.0, min(100.0, power)) # 限制在0-100%
return True
def _start_heating_control(self):
"""启动加热控制线程"""
if not self._running:
self._running = True
self._heating_thread = threading.Thread(target=self._heating_control_loop)
self._heating_thread.daemon = True
self._heating_thread.start()
def _stop_heating_control(self):
"""停止加热控制"""
self._running = False
if self._heating_thread:
self._heating_thread.join(timeout=1.0)
def _heating_control_loop(self):
"""加热控制循环"""
while self._running:
# 如果状态是 Stopped不改变温度
if self._status == "Stopped":
time.sleep(1.0)
continue
temp_diff = self._target_temperature - self._current_temperature
if abs(temp_diff) < 0.1:
self._status = "At Target Temperature"
self._is_heating = False
self._heating_power = 10.0
elif temp_diff > 0:
self._status = "Heating"
self._is_heating = True
self._heating_power = min(100.0, abs(temp_diff) * 2)
self._current_temperature += 0.5
else:
self._status = "Cooling Down"
self._is_heating = False
self._heating_power = 0.0
self._current_temperature -= 0.2
time.sleep(1.0)
def emergency_stop(self):
"""紧急停止"""
self._status = "Emergency Stop"
self._stop_heating_control()
self._is_heating = False
self._heating_power = 0.0
def get_status_info(self) -> dict:
"""获取完整状态信息"""
return {
"current_temperature": self._current_temperature,
"target_temperature": self._target_temperature,
"status": self._status,
"is_heating": self._is_heating,
"heating_power": self._heating_power,
"max_temperature": self._max_temperature,
"vessel": self._vessel,
"purpose": self._purpose,
"stir": self._stir,
"stir_speed": self._stir_speed
}
# 用于测试的主函数
if __name__ == "__main__":
heater = MockHeater()
print("启动加热器测试...")
print(f"初始状态: {heater.get_status_info()}")
# 设置目标温度为80度
heater.set_temperature(80.0)
# 模拟运行15秒
try:
for i in range(15):
time.sleep(1)
status = heater.get_status_info()
print(
f"\r温度: {status['current_temperature']:.1f}°C / {status['target_temperature']:.1f}°C | "
f"功率: {status['heating_power']:.1f}% | 状态: {status['status']}",
end=""
)
except KeyboardInterrupt:
heater.emergency_stop()
print("\n测试被手动停止")
print("\n测试完成")

View File

@@ -0,0 +1,360 @@
import time
import threading
from datetime import datetime, timedelta
class MockPump:
def __init__(self, port: str = "MOCK"):
self.port = port
# 设备基本状态属性
self._current_device = "MockPump1" # 设备标识符
self._status: str = "Idle" # 设备状态Idle, Running, Error, Stopped
self._pump_state: str = "Stopped" # 泵运行状态Running, Stopped, Paused
# 流量相关属性
self._flow_rate: float = 0.0 # 当前流速 (mL/min)
self._target_flow_rate: float = 0.0 # 目标流速 (mL/min)
self._max_flow_rate: float = 100.0 # 最大流速 (mL/min)
self._total_volume: float = 0.0 # 累计流量 (mL)
# 压力相关属性
self._pressure: float = 0.0 # 当前压力 (bar)
self._max_pressure: float = 10.0 # 最大压力 (bar)
# 运行控制线程
self._pump_thread = None
self._running = False
self._thread_lock = threading.Lock()
# 新增 PumpTransfer 相关属性
self._from_vessel: str = ""
self._to_vessel: str = ""
self._transfer_volume: float = 0.0
self._amount: str = ""
self._transfer_time: float = 0.0
self._is_viscous: bool = False
self._rinsing_solvent: str = ""
self._rinsing_volume: float = 0.0
self._rinsing_repeats: int = 0
self._is_solid: bool = False
# 时间追踪
self._start_time: datetime = None
self._time_spent: timedelta = timedelta()
self._time_remaining: timedelta = timedelta()
# ==================== 状态属性 ====================
# 这些属性会被Uni-Lab系统自动识别并定时对外广播
@property
def status(self) -> str:
return self._status
@property
def current_device(self) -> str:
"""当前设备标识符"""
return self._current_device
@property
def pump_state(self) -> str:
return self._pump_state
@property
def flow_rate(self) -> float:
return self._flow_rate
@property
def target_flow_rate(self) -> float:
return self._target_flow_rate
@property
def pressure(self) -> float:
return self._pressure
@property
def total_volume(self) -> float:
return self._total_volume
@property
def max_flow_rate(self) -> float:
return self._max_flow_rate
@property
def max_pressure(self) -> float:
return self._max_pressure
# 添加新的属性访问器
@property
def from_vessel(self) -> str:
return self._from_vessel
@property
def to_vessel(self) -> str:
return self._to_vessel
@property
def transfer_volume(self) -> float:
return self._transfer_volume
@property
def amount(self) -> str:
return self._amount
@property
def transfer_time(self) -> float:
return self._transfer_time
@property
def is_viscous(self) -> bool:
return self._is_viscous
@property
def rinsing_solvent(self) -> str:
return self._rinsing_solvent
@property
def rinsing_volume(self) -> float:
return self._rinsing_volume
@property
def rinsing_repeats(self) -> int:
return self._rinsing_repeats
@property
def is_solid(self) -> bool:
return self._is_solid
# 修改这两个属性装饰器
@property
def time_spent(self) -> float:
"""已用时间(秒)"""
if isinstance(self._time_spent, timedelta):
return self._time_spent.total_seconds()
return float(self._time_spent)
@property
def time_remaining(self) -> float:
"""剩余时间(秒)"""
if isinstance(self._time_remaining, timedelta):
return self._time_remaining.total_seconds()
return float(self._time_remaining)
# ==================== 设备控制方法 ====================
# 这些方法需要在注册表中添加会作为ActionServer接受控制指令
def pump_transfer(self, from_vessel: str, to_vessel: str, volume: float,
amount: str = "", time: float = 0.0, viscous: bool = False,
rinsing_solvent: str = "", rinsing_volume: float = 0.0,
rinsing_repeats: int = 0, solid: bool = False) -> dict:
"""Execute pump transfer operation"""
# Stop any existing operation first
self._stop_pump_operation()
# Set transfer parameters
self._from_vessel = from_vessel
self._to_vessel = to_vessel
self._transfer_volume = float(volume)
self._amount = amount
self._transfer_time = float(time)
self._is_viscous = viscous
self._rinsing_solvent = rinsing_solvent
self._rinsing_volume = float(rinsing_volume)
self._rinsing_repeats = int(rinsing_repeats)
self._is_solid = solid
# Calculate flow rate
if self._transfer_time > 0 and self._transfer_volume > 0:
self._target_flow_rate = (self._transfer_volume / self._transfer_time) * 60.0
else:
self._target_flow_rate = 10.0 if not self._is_viscous else 5.0
# Reset timers and counters
self._start_time = datetime.now()
self._time_spent = timedelta()
self._time_remaining = timedelta(seconds=self._transfer_time)
self._total_volume = 0.0
self._flow_rate = 0.0
# Start pump operation
self._pump_state = "Running"
self._status = "Starting Transfer"
self._running = True
# Start pump operation thread
self._pump_thread = threading.Thread(target=self._pump_operation_loop)
self._pump_thread.daemon = True
self._pump_thread.start()
# Wait briefly to ensure thread starts
time.sleep(0.1)
return {
"success": True,
"status": self._status,
"current_device": self._current_device,
"time_spent": 0.0,
"time_remaining": float(self._transfer_time)
}
def pause_pump(self) -> str:
if self._pump_state != "Running":
self._status = "Error: Pump not running"
return "Error"
self._pump_state = "Paused"
self._status = "Pump Paused"
self._stop_pump_operation()
return "Success"
def resume_pump(self) -> str:
if self._pump_state != "Paused":
self._status = "Error: Pump not paused"
return "Error"
self._pump_state = "Running"
self._status = "Resuming Pump"
self._start_pump_operation()
return "Success"
def reset_volume_counter(self) -> str:
self._total_volume = 0.0
self._status = "Volume counter reset"
return "Success"
def emergency_stop(self) -> str:
self._status = "Emergency Stop"
self._pump_state = "Stopped"
self._stop_pump_operation()
self._flow_rate = 0.0
self._pressure = 0.0
self._target_flow_rate = 0.0
return "Success"
# ==================== 内部控制方法 ====================
def _start_pump_operation(self):
with self._thread_lock:
if not self._running:
self._running = True
self._pump_thread = threading.Thread(target=self._pump_operation_loop)
self._pump_thread.daemon = True
self._pump_thread.start()
def _stop_pump_operation(self):
with self._thread_lock:
self._running = False
if self._pump_thread and self._pump_thread.is_alive():
self._pump_thread.join(timeout=2.0)
def _pump_operation_loop(self):
"""泵运行主循环"""
print("Pump operation loop started") # Debug print
while self._running and self._pump_state == "Running":
try:
# Calculate flow rate adjustment
flow_diff = self._target_flow_rate - self._flow_rate
# Adjust flow rate more aggressively (50% of difference)
adjustment = flow_diff * 0.5
self._flow_rate += adjustment
# Ensure flow rate is within bounds
self._flow_rate = max(0.1, min(self._max_flow_rate, self._flow_rate))
# Update status based on flow rate
if abs(flow_diff) < 0.1:
self._status = "Running at Target Flow Rate"
else:
self._status = "Adjusting Flow Rate"
# Calculate volume increment
volume_increment = (self._flow_rate / 60.0) # mL/s
self._total_volume += volume_increment
# Update time tracking
self._time_spent = datetime.now() - self._start_time
if self._transfer_time > 0:
remaining = self._transfer_time - self._time_spent.total_seconds()
self._time_remaining = timedelta(seconds=max(0, remaining))
# Check completion
if self._total_volume >= self._transfer_volume:
self._status = "Transfer Completed"
self._pump_state = "Stopped"
self._running = False
break
# Update pressure
self._pressure = (self._flow_rate / self._max_flow_rate) * self._max_pressure
print(f"Debug - Flow: {self._flow_rate:.1f}, Volume: {self._total_volume:.1f}") # Debug print
time.sleep(1.0)
except Exception as e:
print(f"Error in pump operation: {str(e)}")
self._status = "Error in pump operation"
self._pump_state = "Stopped"
self._running = False
break
def get_status_info(self) -> dict:
"""
获取完整的设备状态信息
Returns:
dict: 包含所有设备状态的字典
"""
return {
"status": self._status,
"pump_state": self._pump_state,
"flow_rate": self._flow_rate,
"target_flow_rate": self._target_flow_rate,
"pressure": self._pressure,
"total_volume": self._total_volume,
"max_flow_rate": self._max_flow_rate,
"max_pressure": self._max_pressure,
"current_device": self._current_device,
"from_vessel": self._from_vessel,
"to_vessel": self._to_vessel,
"transfer_volume": self._transfer_volume,
"amount": self._amount,
"transfer_time": self._transfer_time,
"is_viscous": self._is_viscous,
"rinsing_solvent": self._rinsing_solvent,
"rinsing_volume": self._rinsing_volume,
"rinsing_repeats": self._rinsing_repeats,
"is_solid": self._is_solid,
"time_spent": self._time_spent.total_seconds(),
"time_remaining": self._time_remaining.total_seconds()
}
# 用于测试的主函数
if __name__ == "__main__":
pump = MockPump()
# 测试基本功能
print("启动泵设备测试...")
print(f"初始状态: {pump.get_status_info()}")
# 设置流速并启动
pump.set_flow_rate(50.0)
pump.start_pump()
# 模拟运行10秒
for i in range(10):
time.sleep(1)
print(f"{i+1}秒: 流速={pump.flow_rate:.1f}mL/min, 压力={pump.pressure:.2f}bar, 状态={pump.status}")
# 测试方向切换
print("切换泵方向...")
pump.emergency_stop()
print("测试完成")

View File

@@ -0,0 +1,390 @@
import time
import threading
import json
class MockRotavap:
"""
模拟旋转蒸发器设备类
这个类模拟了一个实验室旋转蒸发器的行为,包括旋转控制、
真空泵控制、温度控制等功能。参考了现有的 RotavapOne 实现。
"""
def __init__(self, port: str = "MOCK"):
"""
初始化MockRotavap实例
Args:
port (str): 设备端口,默认为"MOCK"表示模拟设备
"""
self.port = port
# 设备基本状态属性
self._status: str = "Idle" # 设备状态Idle, Running, Error, Stopped
# 旋转相关属性
self._rotate_state: str = "Stopped" # 旋转状态Running, Stopped
self._rotate_time: float = 0.0 # 旋转剩余时间 (秒)
self._rotate_speed: float = 0.0 # 旋转速度 (rpm)
self._max_rotate_speed: float = 300.0 # 最大旋转速度 (rpm)
# 真空泵相关属性
self._pump_state: str = "Stopped" # 泵状态Running, Stopped
self._pump_time: float = 0.0 # 泵剩余时间 (秒)
self._vacuum_level: float = 0.0 # 真空度 (mbar)
self._target_vacuum: float = 50.0 # 目标真空度 (mbar)
# 温度相关属性
self._temperature: float = 25.0 # 水浴温度 (°C)
self._target_temperature: float = 25.0 # 目标温度 (°C)
self._max_temperature: float = 180.0 # 最大温度 (°C)
# 运行控制线程
self._operation_thread = None
self._running = False
self._thread_lock = threading.Lock()
# 操作成功标志
self.success: str = "True" # 使用字符串而不是布尔值
# ==================== 状态属性 ====================
# 这些属性会被Uni-Lab系统自动识别并定时对外广播
@property
def status(self) -> str:
return self._status
@property
def rotate_state(self) -> str:
return self._rotate_state
@property
def rotate_time(self) -> float:
return self._rotate_time
@property
def rotate_speed(self) -> float:
return self._rotate_speed
@property
def pump_state(self) -> str:
return self._pump_state
@property
def pump_time(self) -> float:
return self._pump_time
@property
def vacuum_level(self) -> float:
return self._vacuum_level
@property
def temperature(self) -> float:
return self._temperature
@property
def target_temperature(self) -> float:
return self._target_temperature
# ==================== 设备控制方法 ====================
# 这些方法需要在注册表中添加会作为ActionServer接受控制指令
def set_timer(self, command: str) -> str:
"""
设置定时器 - 兼容现有RotavapOne接口
Args:
command (str): JSON格式的命令字符串包含rotate_time和pump_time
Returns:
str: 操作结果状态 ("Success", "Error")
"""
try:
timer = json.loads(command)
rotate_time = timer.get("rotate_time", 0)
pump_time = timer.get("pump_time", 0)
self.success = "False"
self._rotate_time = float(rotate_time)
self._pump_time = float(pump_time)
self.success = "True"
self._status = "Timer Set"
return "Success"
except (json.JSONDecodeError, ValueError, KeyError) as e:
self._status = f"Error: Invalid command format - {str(e)}"
self.success = "False"
return "Error"
def set_rotate_time(self, time_seconds: float) -> str:
"""
设置旋转时间
Args:
time_seconds (float): 旋转时间 (秒)
Returns:
str: 操作结果状态 ("Success", "Error")
"""
self.success = "False"
self._rotate_time = max(0.0, float(time_seconds))
self.success = "True"
self._status = "Rotate time set"
return "Success"
def set_pump_time(self, time_seconds: float) -> str:
"""
设置泵时间
Args:
time_seconds (float): 泵时间 (秒)
Returns:
str: 操作结果状态 ("Success", "Error")
"""
self.success = "False"
self._pump_time = max(0.0, float(time_seconds))
self.success = "True"
self._status = "Pump time set"
return "Success"
def set_rotate_speed(self, speed: float) -> str:
"""
设置旋转速度
Args:
speed (float): 旋转速度 (rpm)
Returns:
str: 操作结果状态 ("Success", "Error")
"""
if speed < 0 or speed > self._max_rotate_speed:
self._status = f"Error: Speed out of range (0-{self._max_rotate_speed})"
return "Error"
self._rotate_speed = speed
self._status = "Rotate speed set"
return "Success"
def set_temperature(self, temperature: float) -> str:
"""
设置水浴温度
Args:
temperature (float): 目标温度 (°C)
Returns:
str: 操作结果状态 ("Success", "Error")
"""
if temperature < 0 or temperature > self._max_temperature:
self._status = f"Error: Temperature out of range (0-{self._max_temperature})"
return "Error"
self._target_temperature = temperature
self._status = "Temperature set"
# 启动操作线程以开始温度控制
self._start_operation()
return "Success"
def start_rotation(self) -> str:
"""
启动旋转
Returns:
str: 操作结果状态 ("Success", "Error")
"""
if self._rotate_time <= 0:
self._status = "Error: No rotate time set"
return "Error"
self._rotate_state = "Running"
self._status = "Rotation started"
return "Success"
def start_pump(self) -> str:
"""
启动真空泵
Returns:
str: 操作结果状态 ("Success", "Error")
"""
if self._pump_time <= 0:
self._status = "Error: No pump time set"
return "Error"
self._pump_state = "Running"
self._status = "Pump started"
return "Success"
def stop_all_operations(self) -> str:
"""
停止所有操作
Returns:
str: 操作结果状态 ("Success", "Error")
"""
self._rotate_state = "Stopped"
self._pump_state = "Stopped"
self._stop_operation()
self._rotate_time = 0.0
self._pump_time = 0.0
self._vacuum_level = 0.0
self._status = "All operations stopped"
return "Success"
def emergency_stop(self) -> str:
"""
紧急停止
Returns:
str: 操作结果状态 ("Success", "Error")
"""
self._status = "Emergency Stop"
self.stop_all_operations()
return "Success"
# ==================== 内部控制方法 ====================
def _start_operation(self):
"""
启动操作线程
这个方法启动一个后台线程来模拟旋蒸的实际运行过程。
"""
with self._thread_lock:
if not self._running:
self._running = True
self._operation_thread = threading.Thread(target=self._operation_loop)
self._operation_thread.daemon = True
self._operation_thread.start()
def _stop_operation(self):
"""
停止操作线程
安全地停止后台运行线程并等待其完成。
"""
with self._thread_lock:
self._running = False
if self._operation_thread and self._operation_thread.is_alive():
self._operation_thread.join(timeout=2.0)
def _operation_loop(self):
"""
操作主循环
这个方法在后台线程中运行,模拟真实旋蒸的工作过程:
1. 时间倒计时
2. 温度控制
3. 真空度控制
4. 状态更新
"""
while self._running:
try:
# 处理旋转时间倒计时
if self._rotate_time > 0:
self._rotate_state = "Running"
self._rotate_time = max(0.0, self._rotate_time - 1.0)
else:
self._rotate_state = "Stopped"
# 处理泵时间倒计时
if self._pump_time > 0:
self._pump_state = "Running"
self._pump_time = max(0.0, self._pump_time - 1.0)
# 模拟真空度变化
if self._vacuum_level > self._target_vacuum:
self._vacuum_level = max(self._target_vacuum, self._vacuum_level - 5.0)
else:
self._pump_state = "Stopped"
# 真空度逐渐回升
self._vacuum_level = min(1013.25, self._vacuum_level + 2.0)
# 模拟温度控制
temp_diff = self._target_temperature - self._temperature
if abs(temp_diff) > 0.5:
if temp_diff > 0:
self._temperature += min(1.0, temp_diff * 0.1)
else:
self._temperature += max(-1.0, temp_diff * 0.1)
# 更新整体状态
if self._rotate_state == "Running" or self._pump_state == "Running":
self._status = "Operating"
elif self._rotate_time > 0 or self._pump_time > 0:
self._status = "Ready"
else:
self._status = "Idle"
# 等待1秒后继续下一次循环
time.sleep(1.0)
except Exception as e:
self._status = f"Error in operation: {str(e)}"
break
# 循环结束时的清理工作
self._status = "Idle"
def get_status_info(self) -> dict:
"""
获取完整的设备状态信息
Returns:
dict: 包含所有设备状态的字典
"""
return {
"status": self._status,
"rotate_state": self._rotate_state,
"rotate_time": self._rotate_time,
"rotate_speed": self._rotate_speed,
"pump_state": self._pump_state,
"pump_time": self._pump_time,
"vacuum_level": self._vacuum_level,
"temperature": self._temperature,
"target_temperature": self._target_temperature,
"success": self.success,
}
# 用于测试的主函数
if __name__ == "__main__":
rotavap = MockRotavap()
# 测试基本功能
print("启动旋转蒸发器测试...")
print(f"初始状态: {rotavap.get_status_info()}")
# 设置定时器
timer_command = '{"rotate_time": 300, "pump_time": 600}'
rotavap.set_timer(timer_command)
# 设置温度和转速
rotavap.set_temperature(60.0)
rotavap.set_rotate_speed(120.0)
# 启动操作
rotavap.start_rotation()
rotavap.start_pump()
# 模拟运行10秒
for i in range(10):
time.sleep(1)
print(
f"{i+1}秒: 旋转={rotavap.rotate_time:.0f}s, 泵={rotavap.pump_time:.0f}s, "
f"温度={rotavap.temperature:.1f}°C, 真空={rotavap.vacuum_level:.1f}mbar"
)
rotavap.emergency_stop()
print("测试完成")

View File

@@ -0,0 +1,399 @@
import time
import threading
from datetime import datetime, timedelta
class MockSeparator:
def __init__(self, port: str = "MOCK"):
self.port = port
# 基本状态属性
self._status: str = "Idle" # 当前总体状态
self._valve_state: str = "Closed" # 阀门状态Open 或 Closed
self._settling_time: float = 0.0 # 静置时间(秒)
# 搅拌相关属性
self._shake_time: float = 0.0 # 剩余摇摆时间(秒)
self._shake_status: str = "Not Shaking" # 摇摆状态
# 用于后台模拟 shake 动作
self._operation_thread = None
self._thread_lock = threading.Lock()
self._running = False
# Separate action 相关属性
self._current_device: str = "MockSeparator1"
self._purpose: str = "" # wash or extract
self._product_phase: str = "" # top or bottom
self._from_vessel: str = ""
self._separation_vessel: str = ""
self._to_vessel: str = ""
self._waste_phase_to_vessel: str = ""
self._solvent: str = ""
self._solvent_volume: float = 0.0
self._through: str = ""
self._repeats: int = 1
self._stir_time: float = 0.0
self._stir_speed: float = 0.0
self._time_spent = timedelta()
self._time_remaining = timedelta()
self._start_time = datetime.now() # 添加这一行
@property
def current_device(self) -> str:
return self._current_device
@property
def purpose(self) -> str:
return self._purpose
@property
def valve_state(self) -> str:
return self._valve_state
@property
def settling_time(self) -> float:
return self._settling_time
@property
def status(self) -> str:
return self._status
@property
def shake_time(self) -> float:
with self._thread_lock:
return self._shake_time
@property
def shake_status(self) -> str:
with self._thread_lock:
return self._shake_status
@property
def product_phase(self) -> str:
return self._product_phase
@property
def from_vessel(self) -> str:
return self._from_vessel
@property
def separation_vessel(self) -> str:
return self._separation_vessel
@property
def to_vessel(self) -> str:
return self._to_vessel
@property
def waste_phase_to_vessel(self) -> str:
return self._waste_phase_to_vessel
@property
def solvent(self) -> str:
return self._solvent
@property
def solvent_volume(self) -> float:
return self._solvent_volume
@property
def through(self) -> str:
return self._through
@property
def repeats(self) -> int:
return self._repeats
@property
def stir_time(self) -> float:
return self._stir_time
@property
def stir_speed(self) -> float:
return self._stir_speed
@property
def time_spent(self) -> float:
if self._running:
self._time_spent = datetime.now() - self._start_time
return self._time_spent.total_seconds()
@property
def time_remaining(self) -> float:
if self._running:
elapsed = (datetime.now() - self._start_time).total_seconds()
total_time = (self._stir_time + self._settling_time + 10) * self._repeats
remain = max(0, total_time - elapsed)
self._time_remaining = timedelta(seconds=remain)
return self._time_remaining.total_seconds()
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 = 0.0, through: str = "",
repeats: int = 1, stir_time: float = 0.0, stir_speed: float = 0.0,
settling_time: float = 60.0) -> dict:
"""
执行分离操作
"""
with self._thread_lock:
# 检查是否已经在运行
if self._running:
return {
"success": False,
"status": "Error: Operation already in progress"
}
# 必填参数验证
if not all([from_vessel, separation_vessel, to_vessel]):
self._status = "Error: Missing required vessel parameters"
return {"success": False}
# 验证参数
if purpose not in ["wash", "extract"]:
self._status = "Error: Invalid purpose"
return {"success": False}
if product_phase not in ["top", "bottom"]:
self._status = "Error: Invalid product phase"
return {"success": False}
# 数值参数验证
try:
solvent_volume = float(solvent_volume)
repeats = int(repeats)
stir_time = float(stir_time)
stir_speed = float(stir_speed)
settling_time = float(settling_time)
except ValueError:
self._status = "Error: Invalid numeric parameters"
return {"success": False}
# 设置参数
self._purpose = purpose
self._product_phase = product_phase
self._from_vessel = from_vessel
self._separation_vessel = separation_vessel
self._to_vessel = to_vessel
self._waste_phase_to_vessel = waste_phase_to_vessel
self._solvent = solvent
self._solvent_volume = float(solvent_volume)
self._through = through
self._repeats = int(repeats)
self._stir_time = float(stir_time)
self._stir_speed = float(stir_speed)
self._settling_time = float(settling_time)
# 重置计时器
self._start_time = datetime.now()
self._time_spent = timedelta()
total_time = (self._stir_time + self._settling_time + 10) * self._repeats
self._time_remaining = timedelta(seconds=total_time)
# 启动分离操作
self._status = "Starting Separation"
self._running = True
# 在锁内创建和启动线程
self._operation_thread = threading.Thread(target=self._operation_loop)
self._operation_thread.daemon = True
self._operation_thread.start()
# 等待确认操作已经开始
time.sleep(0.1) # 短暂等待确保操作线程已启动
return {
"success": True,
"status": self._status,
"current_device": self._current_device,
"time_spent": self._time_spent.total_seconds(),
"time_remaining": self._time_remaining.total_seconds()
}
def shake(self, shake_time: float) -> str:
"""
模拟 shake搅拌操作
- 进入 "Shaking" 状态,倒计时 shake_time 秒
- shake 结束后,进入 "Settling" 状态,静置时间固定为 5 秒
- 最后恢复为 Idle
"""
try:
shake_time = float(shake_time)
except ValueError:
self._status = "Error: Invalid shake time"
return "Error"
with self._thread_lock:
self._status = "Shaking"
self._settling_time = 0.0
self._shake_time = shake_time
self._shake_status = "Shaking"
def _run_shake():
remaining = shake_time
while remaining > 0:
time.sleep(1)
remaining -= 1
with self._thread_lock:
self._shake_time = remaining
with self._thread_lock:
self._status = "Settling"
self._settling_time = 60.0 # 固定静置时间为60秒
self._shake_status = "Settling"
while True:
with self._thread_lock:
if self._settling_time <= 0:
self._status = "Idle"
self._shake_status = "Idle"
break
time.sleep(1)
with self._thread_lock:
self._settling_time = max(0.0, self._settling_time - 1)
self._operation_thread = threading.Thread(target=_run_shake)
self._operation_thread.daemon = True
self._operation_thread.start()
return "Success"
def set_valve(self, command: str) -> str:
"""
阀门控制命令:传入 "open""close"
"""
command = command.lower()
if command == "open":
self._valve_state = "Open"
self._status = "Valve Opened"
elif command == "close":
self._valve_state = "Closed"
self._status = "Valve Closed"
else:
self._status = "Error: Invalid valve command"
return "Error"
return "Success"
def _operation_loop(self):
"""分离操作主循环"""
try:
current_repeat = 1
# 立即更新状态确保不会停留在Starting Separation
with self._thread_lock:
self._status = f"Separation Cycle {current_repeat}/{self._repeats}"
while self._running and current_repeat <= self._repeats:
# 第一步:搅拌
if self._stir_time > 0:
with self._thread_lock:
self._status = f"Stirring (Repeat {current_repeat}/{self._repeats})"
remaining_stir = self._stir_time
while remaining_stir > 0 and self._running:
time.sleep(1)
remaining_stir -= 1
# 第二步:静置
if self._settling_time > 0:
with self._thread_lock:
self._status = f"Settling (Repeat {current_repeat}/{self._repeats})"
remaining_settle = self._settling_time
while remaining_settle > 0 and self._running:
time.sleep(1)
remaining_settle -= 1
# 第三步:打开阀门排出
with self._thread_lock:
self._valve_state = "Open"
self._status = f"Draining (Repeat {current_repeat}/{self._repeats})"
# 模拟排出时间5秒
time.sleep(10)
# 关闭阀门
with self._thread_lock:
self._valve_state = "Closed"
# 检查是否继续下一次重复
if current_repeat < self._repeats:
current_repeat += 1
else:
with self._thread_lock:
self._status = "Separation Complete"
break
except Exception as e:
with self._thread_lock:
self._status = f"Error in separation: {str(e)}"
finally:
with self._thread_lock:
self._running = False
self._valve_state = "Closed"
if self._status == "Starting Separation":
self._status = "Error: Operation failed to start"
elif self._status != "Separation Complete":
self._status = "Stopped"
def stop_operations(self) -> str:
"""停止任何正在执行的操作"""
with self._thread_lock:
self._running = False
if self._operation_thread and self._operation_thread.is_alive():
self._operation_thread.join(timeout=1.0)
self._operation_thread = None
self._settling_time = 0.0
self._status = "Idle"
self._shake_status = "Idle"
self._shake_time = 0.0
self._time_remaining = timedelta()
return "Success"
def get_status_info(self) -> dict:
"""获取当前设备状态信息"""
with self._thread_lock:
current_time = datetime.now()
if self._start_time:
self._time_spent = current_time - self._start_time
return {
"status": self._status,
"valve_state": self._valve_state,
"settling_time": self._settling_time,
"shake_time": self._shake_time,
"shake_status": self._shake_status,
"current_device": self._current_device,
"purpose": self._purpose,
"product_phase": self._product_phase,
"from_vessel": self._from_vessel,
"separation_vessel": self._separation_vessel,
"to_vessel": self._to_vessel,
"waste_phase_to_vessel": self._waste_phase_to_vessel,
"solvent": self._solvent,
"solvent_volume": self._solvent_volume,
"through": self._through,
"repeats": self._repeats,
"stir_time": self._stir_time,
"stir_speed": self._stir_speed,
"time_spent": self._time_spent.total_seconds(),
"time_remaining": self._time_remaining.total_seconds()
}
# 主函数用于测试
if __name__ == "__main__":
separator = MockSeparator()
print("启动简单版分离器测试...")
print("初始状态:", separator.get_status_info())
# 触发 shake 操作,模拟 10 秒的搅拌
print("执行 shake 操作...")
print(separator.shake(10.0))
# 循环显示状态变化
for i in range(20):
time.sleep(1)
info = separator.get_status_info()
print(
f"{i+1}秒: 状态={info['status']}, 静置时间={info['settling_time']:.1f}秒, "
f"阀门状态={info['valve_state']}, shake_time={info['shake_time']:.1f}, "
f"shake_status={info['shake_status']}"
)
# 模拟打开阀门
print("打开阀门...", separator.set_valve("open"))
print("最终状态:", separator.get_status_info())

View File

@@ -0,0 +1,89 @@
import time
class MockSolenoidValve:
"""
模拟电磁阀设备类 - 简化版本
这个类提供了电磁阀的基本功能:开启、关闭和状态查询
"""
def __init__(self, port: str = "MOCK"):
"""
初始化MockSolenoidValve实例
Args:
port (str): 设备端口,默认为"MOCK"表示模拟设备
"""
self.port = port
self._status: str = "Idle"
self._valve_status: str = "Closed" # 阀门位置Open, Closed
@property
def status(self) -> str:
"""设备状态 - 会被自动识别的设备属性"""
return self._status
@property
def valve_status(self) -> str:
"""阀门状态"""
return self._valve_status
def set_valve_status(self, status: str) -> str:
"""
设置阀门位置
Args:
position (str): 阀门位置,可选值:"Open", "Closed"
Returns:
str: 操作结果状态 ("Success", "Error")
"""
if status not in ["Open", "Closed"]:
self._status = "Error: Invalid position"
return "Error"
self._status = "Moving"
time.sleep(1) # 模拟阀门动作时间
self._valve_status = status
self._status = "Idle"
return "Success"
def open_valve(self) -> str:
"""打开阀门"""
return self.set_valve_status("Open")
def close_valve(self) -> str:
"""关闭阀门"""
return self.set_valve_status("Closed")
def get_valve_status(self) -> str:
"""获取阀门位置"""
return self._valve_status
def is_open(self) -> bool:
"""检查阀门是否打开"""
return self._valve_status == "Open"
def is_closed(self) -> bool:
"""检查阀门是否关闭"""
return self._valve_status == "Closed"
# 用于测试的主函数
if __name__ == "__main__":
valve = MockSolenoidValve()
print("启动电磁阀测试...")
print(f"初始状态: 位置={valve.valve_status}, 状态={valve.status}")
# 测试开启阀门
valve.open_valve()
print(f"开启后: 位置={valve.valve_status}, 状态={valve.status}")
# 测试关闭阀门
valve.close_valve()
print(f"关闭后: 位置={valve.valve_status}, 状态={valve.status}")
print("测试完成")

View File

@@ -0,0 +1,307 @@
import time
import threading
class MockStirrer:
def __init__(self, port: str = "MOCK"):
self.port = port
# 设备基本状态属性
self._status: str = "Idle" # 设备状态Idle, Running, Error, Stopped
# 搅拌相关属性
self._stir_speed: float = 0.0 # 当前搅拌速度 (rpm)
self._target_stir_speed: float = 0.0 # 目标搅拌速度 (rpm)
self._max_stir_speed: float = 2000.0 # 最大搅拌速度 (rpm)
self._stir_state: str = "Stopped" # 搅拌状态Running, Stopped
# 温度相关属性
self._temperature: float = 25.0 # 当前温度 (°C)
self._target_temperature: float = 25.0 # 目标温度 (°C)
self._max_temperature: float = 300.0 # 最大温度 (°C)
self._heating_state: str = "Off" # 加热状态On, Off
self._heating_power: float = 0.0 # 加热功率百分比 0-100
# 运行控制线程
self._operation_thread = None
self._running = False
self._thread_lock = threading.Lock()
# ==================== 状态属性 ====================
# 这些属性会被Uni-Lab系统自动识别并定时对外广播
@property
def status(self) -> str:
return self._status
@property
def stir_speed(self) -> float:
return self._stir_speed
@property
def target_stir_speed(self) -> float:
return self._target_stir_speed
@property
def stir_state(self) -> str:
return self._stir_state
@property
def temperature(self) -> float:
"""
当前温度
Returns:
float: 当前温度 (°C)
"""
return self._temperature
@property
def target_temperature(self) -> float:
"""
目标温度
Returns:
float: 目标温度 (°C)
"""
return self._target_temperature
@property
def heating_state(self) -> str:
return self._heating_state
@property
def heating_power(self) -> float:
return self._heating_power
@property
def max_stir_speed(self) -> float:
return self._max_stir_speed
@property
def max_temperature(self) -> float:
return self._max_temperature
# ==================== 设备控制方法 ====================
# 这些方法需要在注册表中添加会作为ActionServer接受控制指令
def set_stir_speed(self, speed: float) -> str:
speed = float(speed) # 确保传入的速度是浮点数
if speed < 0 or speed > self._max_stir_speed:
self._status = f"Error: Speed out of range (0-{self._max_stir_speed})"
return "Error"
self._target_stir_speed = speed
self._status = "Setting Stir Speed"
# 如果设置了非零速度,启动搅拌
if speed > 0:
self._stir_state = "Running"
else:
self._stir_state = "Stopped"
return "Success"
def set_temperature(self, temperature: float) -> str:
temperature = float(temperature) # 确保传入的温度是浮点数
if temperature < 0 or temperature > self._max_temperature:
self._status = f"Error: Temperature out of range (0-{self._max_temperature})"
return "Error"
self._target_temperature = temperature
self._status = "Setting Temperature"
return "Success"
def start_stirring(self) -> str:
if self._target_stir_speed <= 0:
self._status = "Error: No target speed set"
return "Error"
self._stir_state = "Running"
self._status = "Stirring Started"
return "Success"
def stop_stirring(self) -> str:
self._stir_state = "Stopped"
self._target_stir_speed = 0.0
self._status = "Stirring Stopped"
return "Success"
def heating_control(self, heating_state: str = "On") -> str:
if heating_state not in ["On", "Off"]:
self._status = "Error: Invalid heating state"
return "Error"
self._heating_state = heating_state
if heating_state == "On":
self._status = "Heating On"
else:
self._status = "Heating Off"
self._heating_power = 0.0
return "Success"
def stop_all_operations(self) -> str:
self._stir_state = "Stopped"
self._heating_state = "Off"
self._stop_operation()
self._stir_speed = 0.0
self._target_stir_speed = 0.0
self._heating_power = 0.0
self._status = "All operations stopped"
return "Success"
def emergency_stop(self) -> str:
"""
紧急停止
Returns:
str: 操作结果状态 ("Success", "Error")
"""
self._status = "Emergency Stop"
self.stop_all_operations()
return "Success"
# ==================== 内部控制方法 ====================
def _start_operation(self):
with self._thread_lock:
if not self._running:
self._running = True
self._operation_thread = threading.Thread(target=self._operation_loop)
self._operation_thread.daemon = True
self._operation_thread.start()
def _stop_operation(self):
"""
停止操作线程
安全地停止后台运行线程并等待其完成。
"""
with self._thread_lock:
self._running = False
if self._operation_thread and self._operation_thread.is_alive():
self._operation_thread.join(timeout=2.0)
def _operation_loop(self):
while self._running:
try:
# 处理搅拌速度控制
if self._stir_state == "Running":
speed_diff = self._target_stir_speed - self._stir_speed
if abs(speed_diff) < 1.0: # 速度接近目标值
self._stir_speed = self._target_stir_speed
if self._stir_speed > 0:
self._status = "Stirring at Target Speed"
else:
# 模拟速度调节每秒调整10%的差值
adjustment = speed_diff * 0.1
self._stir_speed += adjustment
self._status = "Adjusting Stir Speed"
# 确保速度在合理范围内
self._stir_speed = max(0.0, min(self._max_stir_speed, self._stir_speed))
else:
# 搅拌停止时速度逐渐降为0
if self._stir_speed > 0:
self._stir_speed = max(0.0, self._stir_speed - 50.0) # 每秒减少50rpm
# 处理温度控制
if self._heating_state == "On":
temp_diff = self._target_temperature - self._temperature
if abs(temp_diff) < 0.5: # 温度接近目标值
self._heating_power = 20.0 # 维持温度的最小功率
elif temp_diff > 0: # 需要加热
# 根据温差调整加热功率
if temp_diff > 50:
self._heating_power = 100.0
elif temp_diff > 20:
self._heating_power = 80.0
elif temp_diff > 10:
self._heating_power = 60.0
else:
self._heating_power = 40.0
# 模拟加热过程
heating_rate = self._heating_power / 100.0 * 1.5 # 最大每秒升温1.5度
self._temperature += heating_rate
else: # 目标温度低于当前温度
self._heating_power = 0.0
# 自然冷却
self._temperature -= 0.1
else:
self._heating_power = 0.0
# 自然冷却到室温
if self._temperature > 25.0:
self._temperature -= 0.2
# 限制温度范围
self._temperature = max(20.0, min(self._max_temperature, self._temperature))
# 更新整体状态
if self._stir_state == "Running" and self._heating_state == "On":
self._status = "Stirring and Heating"
elif self._stir_state == "Running":
self._status = "Stirring Only"
elif self._heating_state == "On":
self._status = "Heating Only"
else:
self._status = "Idle"
# 等待1秒后继续下一次循环
time.sleep(1.0)
except Exception as e:
self._status = f"Error in operation: {str(e)}"
break
# 循环结束时的清理工作
self._status = "Idle"
def get_status_info(self) -> dict:
return {
"status": self._status,
"stir_speed": self._stir_speed,
"target_stir_speed": self._target_stir_speed,
"stir_state": self._stir_state,
"temperature": self._temperature,
"target_temperature": self._target_temperature,
"heating_state": self._heating_state,
"heating_power": self._heating_power,
"max_stir_speed": self._max_stir_speed,
"max_temperature": self._max_temperature,
}
# 用于测试的主函数
if __name__ == "__main__":
stirrer = MockStirrer()
# 测试基本功能
print("启动搅拌器测试...")
print(f"初始状态: {stirrer.get_status_info()}")
# 设置搅拌速度和温度
stirrer.set_stir_speed(800.0)
stirrer.set_temperature(60.0)
stirrer.heating_control("On")
# 模拟运行15秒
for i in range(15):
time.sleep(1)
print(
f"{i+1}秒: 速度={stirrer.stir_speed:.0f}rpm, 温度={stirrer.temperature:.1f}°C, "
f"功率={stirrer.heating_power:.1f}%, 状态={stirrer.status}"
)
stirrer.emergency_stop()
print("测试完成")

View File

@@ -0,0 +1,229 @@
import time
import threading
from datetime import datetime, timedelta
class MockStirrer_new:
def __init__(self, port: str = "MOCK"):
self.port = port
# 基本状态属性
self._status: str = "Idle"
self._vessel: str = ""
self._purpose: str = ""
# 搅拌相关属性
self._stir_speed: float = 0.0
self._target_stir_speed: float = 0.0
self._max_stir_speed: float = 2000.0
self._stir_state: str = "Stopped"
# 计时相关
self._stir_time: float = 0.0
self._settling_time: float = 0.0
self._start_time = datetime.now()
self._time_remaining = timedelta()
# 运行控制
self._operation_thread = None
self._running = False
self._thread_lock = threading.Lock()
# 创建操作线程
self._operation_thread = threading.Thread(target=self._operation_loop)
self._operation_thread.daemon = True
self._operation_thread.start()
# ==================== 状态属性 ====================
@property
def status(self) -> str:
return self._status
@property
def stir_speed(self) -> float:
return self._stir_speed
@property
def target_stir_speed(self) -> float:
return self._target_stir_speed
@property
def stir_state(self) -> str:
return self._stir_state
@property
def vessel(self) -> str:
return self._vessel
@property
def purpose(self) -> str:
return self._purpose
@property
def stir_time(self) -> float:
return self._stir_time
@property
def settling_time(self) -> float:
return self._settling_time
@property
def max_stir_speed(self) -> float:
return self._max_stir_speed
@property
def progress(self) -> float:
"""返回当前操作的进度0-100"""
if not self._running:
return 0.0
elapsed = (datetime.now() - self._start_time).total_seconds()
total_time = self._stir_time + self._settling_time
if total_time <= 0:
return 100.0
return min(100.0, (elapsed / total_time) * 100)
# ==================== Action Server 方法 ====================
def start_stir(self, vessel: str, stir_speed: float = 0.0, purpose: str = "") -> dict:
"""
StartStir.action 对应的方法
"""
with self._thread_lock:
if self._running:
return {
"success": False,
"message": "Operation already in progress"
}
try:
# 重置所有参数
self._vessel = vessel
self._purpose = purpose
self._stir_time = 0.0 # 连续搅拌模式下不设置搅拌时间
self._settling_time = 0.0
self._start_time = datetime.now() # 重置开始时间
if stir_speed > 0:
self._target_stir_speed = min(stir_speed, self._max_stir_speed)
self._stir_state = "Running"
self._status = "Stirring Started"
self._running = True
return {
"success": True,
"message": "Stirring started successfully"
}
except Exception as e:
return {
"success": False,
"message": f"Error: {str(e)}"
}
def stir(self, stir_time: float, stir_speed: float, settling_time: float) -> dict:
"""
Stir.action 对应的方法
"""
with self._thread_lock:
try:
# 如果已经在运行,先停止当前操作
if self._running:
self._running = False
self._stir_state = "Stopped"
self._target_stir_speed = 0.0
time.sleep(0.1) # 给一个短暂的停止时间
# 重置所有参数
self._stir_time = float(stir_time)
self._settling_time = float(settling_time)
self._target_stir_speed = min(float(stir_speed), self._max_stir_speed)
self._start_time = datetime.now() # 重置开始时间
self._stir_state = "Running"
self._status = "Stirring"
self._running = True
return {"success": True}
except ValueError:
self._status = "Error: Invalid parameters"
return {"success": False}
def stop_stir(self, vessel: str) -> dict:
"""
StopStir.action 对应的方法
"""
with self._thread_lock:
if vessel != self._vessel:
return {
"success": False,
"message": "Vessel mismatch"
}
self._running = False
self._stir_state = "Stopped"
self._target_stir_speed = 0.0
self._status = "Stirring Stopped"
return {
"success": True,
"message": "Stirring stopped successfully"
}
# ==================== 内部控制方法 ====================
def _operation_loop(self):
"""操作主循环"""
while True:
try:
current_time = datetime.now()
with self._thread_lock: # 添加锁保护
if self._stir_state == "Running":
# 实际搅拌逻辑
speed_diff = self._target_stir_speed - self._stir_speed
if abs(speed_diff) > 0.1:
adjustment = speed_diff * 0.1
self._stir_speed += adjustment
else:
self._stir_speed = self._target_stir_speed
# 更新进度
if self._running:
if self._stir_time > 0: # 定时搅拌模式
elapsed = (current_time - self._start_time).total_seconds()
if elapsed >= self._stir_time + self._settling_time:
self._running = False
self._stir_state = "Stopped"
self._target_stir_speed = 0.0
self._stir_speed = 0.0
self._status = "Stirring Complete"
elif elapsed >= self._stir_time:
self._status = "Settling"
else: # 连续搅拌模式
self._status = "Stirring"
else:
# 停止状态下慢慢降低速度
if self._stir_speed > 0:
self._stir_speed = max(0, self._stir_speed - 20.0)
time.sleep(0.1)
except Exception as e:
print(f"Error in operation loop: {str(e)}") # 添加错误输出
self._status = f"Error: {str(e)}"
time.sleep(1.0) # 错误发生时等待较长时间
def get_status_info(self) -> dict:
"""获取设备状态信息"""
return {
"status": self._status,
"vessel": self._vessel,
"purpose": self._purpose,
"stir_speed": self._stir_speed,
"target_stir_speed": self._target_stir_speed,
"stir_state": self._stir_state,
"stir_time": self._stir_time, # 添加
"settling_time": self._settling_time, # 添加
"progress": self.progress,
"max_stir_speed": self._max_stir_speed
}

View File

@@ -0,0 +1,410 @@
import time
import threading
class MockVacuum:
"""
模拟真空泵设备类
这个类模拟了一个实验室真空泵的行为,包括真空度控制、
压力监测、运行状态管理等功能。参考了现有的 VacuumPumpMock 实现。
"""
def __init__(self, port: str = "MOCK"):
"""
初始化MockVacuum实例
Args:
port (str): 设备端口,默认为"MOCK"表示模拟设备
"""
self.port = port
# 设备基本状态属性
self._status: str = "Idle" # 设备状态Idle, Running, Error, Stopped
self._power_state: str = "Off" # 电源状态On, Off
self._pump_state: str = "Stopped" # 泵运行状态Running, Stopped, Paused
# 真空相关属性
self._vacuum_level: float = 1013.25 # 当前真空度 (mbar) - 大气压开始
self._target_vacuum: float = 50.0 # 目标真空度 (mbar)
self._min_vacuum: float = 1.0 # 最小真空度 (mbar)
self._max_vacuum: float = 1013.25 # 最大真空度 (mbar) - 大气压
# 泵性能相关属性
self._pump_speed: float = 0.0 # 泵速 (L/s)
self._max_pump_speed: float = 100.0 # 最大泵速 (L/s)
self._pump_efficiency: float = 95.0 # 泵效率百分比
# 运行控制线程
self._vacuum_thread = None
self._running = False
self._thread_lock = threading.Lock()
# ==================== 状态属性 ====================
# 这些属性会被Uni-Lab系统自动识别并定时对外广播
@property
def status(self) -> str:
"""
设备状态 - 会被自动识别的设备属性
Returns:
str: 当前设备状态 (Idle, Running, Error, Stopped)
"""
return self._status
@property
def power_state(self) -> str:
"""
电源状态
Returns:
str: 电源状态 (On, Off)
"""
return self._power_state
@property
def pump_state(self) -> str:
"""
泵运行状态
Returns:
str: 泵状态 (Running, Stopped, Paused)
"""
return self._pump_state
@property
def vacuum_level(self) -> float:
"""
当前真空度
Returns:
float: 当前真空度 (mbar)
"""
return self._vacuum_level
@property
def target_vacuum(self) -> float:
"""
目标真空度
Returns:
float: 目标真空度 (mbar)
"""
return self._target_vacuum
@property
def pump_speed(self) -> float:
"""
泵速
Returns:
float: 泵速 (L/s)
"""
return self._pump_speed
@property
def pump_efficiency(self) -> float:
"""
泵效率
Returns:
float: 泵效率百分比
"""
return self._pump_efficiency
@property
def max_pump_speed(self) -> float:
"""
最大泵速
Returns:
float: 最大泵速 (L/s)
"""
return self._max_pump_speed
# ==================== 设备控制方法 ====================
# 这些方法需要在注册表中添加会作为ActionServer接受控制指令
def power_control(self, power_state: str = "On") -> str:
"""
电源控制方法
Args:
power_state (str): 电源状态,可选值:"On", "Off"
Returns:
str: 操作结果状态 ("Success", "Error")
"""
if power_state not in ["On", "Off"]:
self._status = "Error: Invalid power state"
return "Error"
self._power_state = power_state
if power_state == "On":
self._status = "Power On"
self._start_vacuum_operation()
else:
self._status = "Power Off"
self.stop_vacuum()
return "Success"
def set_vacuum_level(self, vacuum_level: float) -> str:
"""
设置目标真空度
Args:
vacuum_level (float): 目标真空度 (mbar)
Returns:
str: 操作结果状态 ("Success", "Error")
"""
try:
vacuum_level = float(vacuum_level)
except ValueError:
self._status = "Error: Invalid vacuum level"
return "Error"
if self._power_state != "On":
self._status = "Error: Power Off"
return "Error"
if vacuum_level < self._min_vacuum or vacuum_level > self._max_vacuum:
self._status = f"Error: Vacuum level out of range ({self._min_vacuum}-{self._max_vacuum})"
return "Error"
self._target_vacuum = vacuum_level
self._status = "Setting Vacuum Level"
return "Success"
def start_vacuum(self) -> str:
"""
启动真空泵
Returns:
str: 操作结果状态 ("Success", "Error")
"""
if self._power_state != "On":
self._status = "Error: Power Off"
return "Error"
self._pump_state = "Running"
self._status = "Starting Vacuum Pump"
self._start_vacuum_operation()
return "Success"
def stop_vacuum(self) -> str:
"""
停止真空泵
Returns:
str: 操作结果状态 ("Success", "Error")
"""
self._pump_state = "Stopped"
self._status = "Stopping Vacuum Pump"
self._stop_vacuum_operation()
self._pump_speed = 0.0
return "Success"
def pause_vacuum(self) -> str:
"""
暂停真空泵
Returns:
str: 操作结果状态 ("Success", "Error")
"""
if self._pump_state != "Running":
self._status = "Error: Pump not running"
return "Error"
self._pump_state = "Paused"
self._status = "Vacuum Pump Paused"
self._stop_vacuum_operation()
return "Success"
def resume_vacuum(self) -> str:
"""
恢复真空泵运行
Returns:
str: 操作结果状态 ("Success", "Error")
"""
if self._pump_state != "Paused":
self._status = "Error: Pump not paused"
return "Error"
if self._power_state != "On":
self._status = "Error: Power Off"
return "Error"
self._pump_state = "Running"
self._status = "Resuming Vacuum Pump"
self._start_vacuum_operation()
return "Success"
def vent_to_atmosphere(self) -> str:
"""
通大气 - 将真空度恢复到大气压
Returns:
str: 操作结果状态 ("Success", "Error")
"""
self._target_vacuum = self._max_vacuum # 设置为大气压
self._status = "Venting to Atmosphere"
return "Success"
def emergency_stop(self) -> str:
"""
紧急停止
Returns:
str: 操作结果状态 ("Success", "Error")
"""
self._status = "Emergency Stop"
self._pump_state = "Stopped"
self._stop_vacuum_operation()
self._pump_speed = 0.0
return "Success"
# ==================== 内部控制方法 ====================
def _start_vacuum_operation(self):
"""
启动真空操作线程
这个方法启动一个后台线程来模拟真空泵的实际运行过程。
"""
with self._thread_lock:
if not self._running and self._power_state == "On":
self._running = True
self._vacuum_thread = threading.Thread(target=self._vacuum_operation_loop)
self._vacuum_thread.daemon = True
self._vacuum_thread.start()
def _stop_vacuum_operation(self):
"""
停止真空操作线程
安全地停止后台运行线程并等待其完成。
"""
with self._thread_lock:
self._running = False
if self._vacuum_thread and self._vacuum_thread.is_alive():
self._vacuum_thread.join(timeout=2.0)
def _vacuum_operation_loop(self):
"""
真空操作主循环
这个方法在后台线程中运行,模拟真空泵的工作过程:
1. 检查电源状态和运行状态
2. 如果泵状态为 "Running",根据目标真空调整泵速和真空度
3. 否则等待
"""
while self._running and self._power_state == "On":
try:
with self._thread_lock:
# 只有泵状态为 Running 时才进行更新
if self._pump_state == "Running":
vacuum_diff = self._vacuum_level - self._target_vacuum
if abs(vacuum_diff) < 1.0: # 真空度接近目标值
self._status = "At Target Vacuum"
self._pump_speed = self._max_pump_speed * 0.2 # 维持真空的最小泵速
elif vacuum_diff > 0: # 需要抽真空(降低压力)
self._status = "Pumping Down"
if vacuum_diff > 500:
self._pump_speed = self._max_pump_speed
elif vacuum_diff > 100:
self._pump_speed = self._max_pump_speed * 0.8
elif vacuum_diff > 50:
self._pump_speed = self._max_pump_speed * 0.6
else:
self._pump_speed = self._max_pump_speed * 0.4
# 根据泵速和效率计算真空降幅
pump_rate = (self._pump_speed / self._max_pump_speed) * self._pump_efficiency / 100.0
vacuum_reduction = pump_rate * 10.0 # 每秒最大降低10 mbar
self._vacuum_level = max(self._target_vacuum, self._vacuum_level - vacuum_reduction)
else: # 目标真空度高于当前值,需要通气
self._status = "Venting"
self._pump_speed = 0.0
self._vacuum_level = min(self._target_vacuum, self._vacuum_level + 5.0)
# 限制真空度范围
self._vacuum_level = max(self._min_vacuum, min(self._max_vacuum, self._vacuum_level))
else:
# 当泵状态不是 Running 时,可保持原状态
self._status = "Vacuum Pump Not Running"
# 释放锁后等待1秒钟
time.sleep(1.0)
except Exception as e:
with self._thread_lock:
self._status = f"Error in vacuum operation: {str(e)}"
break
# 循环结束后的清理工作
if self._pump_state == "Running":
self._status = "Idle"
# 停止泵后,真空度逐渐回升到大气压
while self._vacuum_level < self._max_vacuum * 0.9:
with self._thread_lock:
self._vacuum_level += 2.0
time.sleep(0.1)
def get_status_info(self) -> dict:
"""
获取完整的设备状态信息
Returns:
dict: 包含所有设备状态的字典
"""
return {
"status": self._status,
"power_state": self._power_state,
"pump_state": self._pump_state,
"vacuum_level": self._vacuum_level,
"target_vacuum": self._target_vacuum,
"pump_speed": self._pump_speed,
"pump_efficiency": self._pump_efficiency,
"max_pump_speed": self._max_pump_speed,
}
# 用于测试的主函数
if __name__ == "__main__":
vacuum = MockVacuum()
# 测试基本功能
print("启动真空泵测试...")
vacuum.power_control("On")
print(f"初始状态: {vacuum.get_status_info()}")
# 设置目标真空度并启动
vacuum.set_vacuum_level(10.0) # 设置为10mbar
vacuum.start_vacuum()
# 模拟运行15秒
for i in range(15):
time.sleep(1)
print(
f"{i+1}秒: 真空度={vacuum.vacuum_level:.1f}mbar, 泵速={vacuum.pump_speed:.1f}L/s, 状态={vacuum.status}"
)
# 测试通大气
print("测试通大气...")
vacuum.vent_to_atmosphere()
# 继续运行5秒观察通大气过程
for i in range(5):
time.sleep(1)
print(f"通大气第{i+1}秒: 真空度={vacuum.vacuum_level:.1f}mbar, 状态={vacuum.status}")
vacuum.emergency_stop()
print("测试完成")

View File

@@ -12,10 +12,8 @@ class VacuumPumpMock:
def get_status(self) -> str: def get_status(self) -> str:
return self._status return self._status
def set_status(self, position): def set_status(self, string):
time.sleep(5) self._status = string
self._status = position
time.sleep(5) time.sleep(5)
def open(self): def open(self):

View File

View File

@@ -0,0 +1,213 @@
import asyncio
import logging
import time as time_module
from typing import Dict, Any, Optional
class VirtualCentrifuge:
"""Virtual centrifuge device - 简化版,只保留核心功能"""
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_centrifuge"
self.config = config or {}
self.logger = logging.getLogger(f"VirtualCentrifuge.{self.device_id}")
self.data = {}
# 从config或kwargs中获取配置参数
self.port = self.config.get("port") or kwargs.get("port", "VIRTUAL")
self._max_speed = self.config.get("max_speed") or kwargs.get("max_speed", 15000.0)
self._max_temp = self.config.get("max_temp") or kwargs.get("max_temp", 40.0)
self._min_temp = self.config.get("min_temp") or kwargs.get("min_temp", 4.0)
# 处理其他kwargs参数
skip_keys = {"port", "max_speed", "max_temp", "min_temp"}
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 centrifuge"""
self.logger.info(f"Initializing virtual centrifuge {self.device_id}")
# 只保留核心状态
self.data.update({
"status": "Idle",
"centrifuge_state": "Stopped", # Stopped, Running, Completed, Error
"current_speed": 0.0,
"target_speed": 0.0,
"current_temp": 25.0,
"target_temp": 25.0,
"time_remaining": 0.0,
"progress": 0.0,
"message": "Ready for centrifugation"
})
return True
async def cleanup(self) -> bool:
"""Cleanup virtual centrifuge"""
self.logger.info(f"Cleaning up virtual centrifuge {self.device_id}")
self.data.update({
"status": "Offline",
"centrifuge_state": "Offline",
"current_speed": 0.0,
"current_temp": 25.0,
"message": "System offline"
})
return True
async def centrifuge(
self,
vessel: str,
speed: float,
time: float,
temp: float = 25.0
) -> bool:
"""Execute centrifuge action - 简化的离心流程"""
self.logger.info(f"Centrifuge: vessel={vessel}, speed={speed} rpm, time={time}s, temp={temp}°C")
# 验证参数
if speed > self._max_speed or speed < 100.0:
error_msg = f"离心速度 {speed} rpm 超出范围 (100-{self._max_speed} rpm)"
self.logger.error(error_msg)
self.data.update({
"status": f"Error: {error_msg}",
"centrifuge_state": "Error",
"message": error_msg
})
return False
if temp > self._max_temp or temp < self._min_temp:
error_msg = f"温度 {temp}°C 超出范围 ({self._min_temp}-{self._max_temp}°C)"
self.logger.error(error_msg)
self.data.update({
"status": f"Error: {error_msg}",
"centrifuge_state": "Error",
"message": error_msg
})
return False
# 开始离心
self.data.update({
"status": f"离心中: {vessel}",
"centrifuge_state": "Running",
"current_speed": speed,
"target_speed": speed,
"current_temp": temp,
"target_temp": temp,
"time_remaining": time,
"progress": 0.0,
"message": f"Centrifuging {vessel} at {speed} rpm, {temp}°C"
})
try:
# 离心过程 - 实时更新进度
start_time = time_module.time()
total_time = time
while True:
current_time = time_module.time()
elapsed = current_time - start_time
remaining = max(0, total_time - elapsed)
progress = min(100.0, (elapsed / total_time) * 100)
# 更新状态
self.data.update({
"time_remaining": remaining,
"progress": progress,
"status": f"离心中: {vessel} | {speed} rpm | {temp}°C | {progress:.1f}% | 剩余: {remaining:.0f}s",
"message": f"Centrifuging: {progress:.1f}% complete, {remaining:.0f}s remaining"
})
# 时间到了,退出循环
if remaining <= 0:
break
# 每秒更新一次
await asyncio.sleep(1.0)
# 离心完成
self.data.update({
"status": f"离心完成: {vessel} | {speed} rpm | {time}s",
"centrifuge_state": "Completed",
"progress": 100.0,
"time_remaining": 0.0,
"current_speed": 0.0, # 停止旋转
"current_temp": 25.0, # 恢复室温
"message": f"Centrifugation completed: {vessel} at {speed} rpm for {time}s"
})
self.logger.info(f"Centrifugation completed: {vessel} at {speed} rpm for {time}s")
return True
except Exception as e:
# 出错处理
self.logger.error(f"Error during centrifugation: {str(e)}")
self.data.update({
"status": f"离心错误: {str(e)}",
"centrifuge_state": "Error",
"current_speed": 0.0,
"current_temp": 25.0,
"progress": 0.0,
"time_remaining": 0.0,
"message": f"Centrifugation failed: {str(e)}"
})
return False
# === 核心状态属性 ===
@property
def status(self) -> str:
return self.data.get("status", "Unknown")
@property
def centrifuge_state(self) -> str:
return self.data.get("centrifuge_state", "Unknown")
@property
def current_speed(self) -> float:
return self.data.get("current_speed", 0.0)
@property
def target_speed(self) -> float:
return self.data.get("target_speed", 0.0)
@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_speed(self) -> float:
return self._max_speed
@property
def max_temp(self) -> float:
return self._max_temp
@property
def min_temp(self) -> float:
return self._min_temp
@property
def time_remaining(self) -> float:
return self.data.get("time_remaining", 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,132 @@
import asyncio
import logging
from typing import Dict, Any, Optional
class VirtualColumn:
"""Virtual column device for RunColumn protocol"""
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')
# 设置默认值
self.device_id = device_id or "unknown_column"
self.config = config or {}
self.logger = logging.getLogger(f"VirtualColumn.{self.device_id}")
self.data = {}
# 从config或kwargs中获取配置参数
self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL')
self._max_flow_rate = self.config.get('max_flow_rate') or kwargs.get('max_flow_rate', 10.0)
self._column_length = self.config.get('column_length') or kwargs.get('column_length', 25.0)
self._column_diameter = self.config.get('column_diameter') or kwargs.get('column_diameter', 2.0)
print(f"=== VirtualColumn {self.device_id} created with max_flow_rate={self._max_flow_rate}, length={self._column_length}cm ===")
async def initialize(self) -> bool:
"""Initialize virtual column"""
self.logger.info(f"Initializing virtual column {self.device_id}")
self.data.update({
"status": "Idle",
"column_state": "Ready",
"current_flow_rate": 0.0,
"max_flow_rate": self._max_flow_rate,
"column_length": self._column_length,
"column_diameter": self._column_diameter,
"processed_volume": 0.0,
"progress": 0.0,
"current_status": "Ready"
})
return True
async def cleanup(self) -> bool:
"""Cleanup virtual column"""
self.logger.info(f"Cleaning up virtual column {self.device_id}")
return True
async def run_column(self, from_vessel: str, to_vessel: str, column: str) -> bool:
"""Execute column chromatography run - matches RunColumn action"""
self.logger.info(f"Running column separation: {from_vessel} -> {to_vessel} using {column}")
# 更新设备状态
self.data.update({
"status": "Running",
"column_state": "Separating",
"current_status": "Column separation in progress",
"progress": 0.0,
"processed_volume": 0.0
})
# 模拟柱层析分离过程
# 假设处理时间基于流速和柱子长度
separation_time = (self._column_length * 2) / self._max_flow_rate # 简化计算
steps = 20 # 分20个步骤模拟分离过程
step_time = separation_time / steps
for i in range(steps):
await asyncio.sleep(step_time)
progress = (i + 1) / steps * 100
volume_processed = (i + 1) * 5.0 # 假设每步处理5mL
# 更新状态
self.data.update({
"progress": progress,
"processed_volume": volume_processed,
"current_status": f"Column separation: {progress:.1f}% - Processing {volume_processed:.1f}mL"
})
self.logger.info(f"Column separation progress: {progress:.1f}%")
# 分离完成
self.data.update({
"status": "Idle",
"column_state": "Ready",
"current_status": "Column separation completed",
"progress": 100.0
})
self.logger.info(f"Column separation completed: {from_vessel} -> {to_vessel}")
return True
# 状态属性
@property
def status(self) -> str:
return self.data.get("status", "Unknown")
@property
def column_state(self) -> str:
return self.data.get("column_state", "Unknown")
@property
def current_flow_rate(self) -> float:
return self.data.get("current_flow_rate", 0.0)
@property
def max_flow_rate(self) -> float:
return self.data.get("max_flow_rate", 0.0)
@property
def column_length(self) -> float:
return self.data.get("column_length", 0.0)
@property
def column_diameter(self) -> float:
return self.data.get("column_diameter", 0.0)
@property
def processed_volume(self) -> float:
return self.data.get("processed_volume", 0.0)
@property
def progress(self) -> float:
return self.data.get("progress", 0.0)
@property
def current_status(self) -> str:
return self.data.get("current_status", "Ready")

View File

@@ -0,0 +1,221 @@
import asyncio
import logging
import time as time_module
from typing import Dict, Any, Optional
class VirtualFilter:
"""Virtual filter device - 完全按照 Filter.action 规范"""
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_filter"
self.config = config or {}
self.logger = logging.getLogger(f"VirtualFilter.{self.device_id}")
self.data = {}
# 从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', 100.0)
self._max_stir_speed = self.config.get('max_stir_speed') or kwargs.get('max_stir_speed', 1000.0)
self._max_volume = self.config.get('max_volume') or kwargs.get('max_volume', 500.0)
# 处理其他kwargs参数
skip_keys = {'port', 'max_temp', 'max_stir_speed', 'max_volume'}
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 filter"""
self.logger.info(f"Initializing virtual filter {self.device_id}")
# 按照 Filter.action 的 feedback 字段初始化
self.data.update({
"status": "Idle",
"progress": 0.0, # Filter.action feedback
"current_temp": 25.0, # Filter.action feedback
"filtered_volume": 0.0, # Filter.action feedback
"current_status": "Ready for filtration", # Filter.action feedback
"message": "Ready for filtration"
})
return True
async def cleanup(self) -> bool:
"""Cleanup virtual filter"""
self.logger.info(f"Cleaning up virtual filter {self.device_id}")
self.data.update({
"status": "Offline",
"current_status": "System offline",
"message": "System offline"
})
return True
async def filter(
self,
vessel: str,
filtrate_vessel: str = "",
stir: bool = False,
stir_speed: float = 300.0,
temp: float = 25.0,
continue_heatchill: bool = False,
volume: float = 0.0
) -> bool:
"""Execute filter action - 完全按照 Filter.action 参数"""
self.logger.info(f"Filter: vessel={vessel}, filtrate_vessel={filtrate_vessel}")
self.logger.info(f" stir={stir}, stir_speed={stir_speed}, temp={temp}")
self.logger.info(f" continue_heatchill={continue_heatchill}, volume={volume}")
# 验证参数
if temp > self._max_temp or temp < 4.0:
error_msg = f"温度 {temp}°C 超出范围 (4-{self._max_temp}°C)"
self.logger.error(error_msg)
self.data.update({
"status": f"Error: {error_msg}",
"current_status": f"Error: {error_msg}",
"message": error_msg
})
return False
if stir and stir_speed > self._max_stir_speed:
error_msg = f"搅拌速度 {stir_speed} RPM 超出范围 (0-{self._max_stir_speed} RPM)"
self.logger.error(error_msg)
self.data.update({
"status": f"Error: {error_msg}",
"current_status": f"Error: {error_msg}",
"message": error_msg
})
return False
if volume > self._max_volume:
error_msg = f"过滤体积 {volume} mL 超出范围 (0-{self._max_volume} mL)"
self.logger.error(error_msg)
self.data.update({
"status": f"Error: {error_msg}",
"current_status": f"Error: {error_msg}",
"message": error_msg
})
return False
# 开始过滤
filter_volume = volume if volume > 0 else 50.0
self.data.update({
"status": f"过滤中: {vessel}",
"current_temp": temp,
"filtered_volume": 0.0,
"progress": 0.0,
"current_status": f"Filtering {vessel}{filtrate_vessel}",
"message": f"Starting filtration: {vessel}{filtrate_vessel}"
})
try:
# 过滤过程 - 实时更新进度
start_time = time_module.time()
# 根据体积和搅拌估算过滤时间
base_time = filter_volume / 5.0 # 5mL/s 基础速度
if stir:
base_time *= 0.8 # 搅拌加速过滤
if temp > 50.0:
base_time *= 0.7 # 高温加速过滤
filter_time = max(base_time, 10.0) # 最少10秒
while True:
current_time = time_module.time()
elapsed = current_time - start_time
remaining = max(0, filter_time - elapsed)
progress = min(100.0, (elapsed / filter_time) * 100)
current_filtered = (progress / 100.0) * filter_volume
# 更新状态 - 按照 Filter.action feedback 字段
status_msg = f"过滤中: {vessel}"
if stir:
status_msg += f" | 搅拌: {stir_speed} RPM"
status_msg += f" | {temp}°C | {progress:.1f}% | 已过滤: {current_filtered:.1f}mL"
self.data.update({
"progress": progress, # Filter.action feedback
"current_temp": temp, # Filter.action feedback
"filtered_volume": current_filtered, # Filter.action feedback
"current_status": f"Filtering: {progress:.1f}% complete", # Filter.action feedback
"status": status_msg,
"message": f"Filtering: {progress:.1f}% complete, {current_filtered:.1f}mL filtered"
})
if remaining <= 0:
break
await asyncio.sleep(1.0)
# 过滤完成
final_temp = temp if continue_heatchill else 25.0
final_status = f"过滤完成: {vessel} | {filter_volume}mL → {filtrate_vessel}"
if continue_heatchill:
final_status += " | 继续加热搅拌"
self.data.update({
"status": final_status,
"progress": 100.0, # Filter.action feedback
"current_temp": final_temp, # Filter.action feedback
"filtered_volume": filter_volume, # Filter.action feedback
"current_status": f"Filtration completed: {filter_volume}mL", # Filter.action feedback
"message": f"Filtration completed: {filter_volume}mL filtered from {vessel}"
})
self.logger.info(f"Filtration completed: {filter_volume}mL from {vessel} to {filtrate_vessel}")
return True
except Exception as e:
self.logger.error(f"Error during filtration: {str(e)}")
self.data.update({
"status": f"过滤错误: {str(e)}",
"current_status": f"Filtration failed: {str(e)}",
"message": f"Filtration failed: {str(e)}"
})
return False
# === 核心状态属性 - 按照 Filter.action feedback 字段 ===
@property
def status(self) -> str:
return self.data.get("status", "Unknown")
@property
def progress(self) -> float:
"""Filter.action feedback 字段"""
return self.data.get("progress", 0.0)
@property
def current_temp(self) -> float:
"""Filter.action feedback 字段"""
return self.data.get("current_temp", 25.0)
@property
def filtered_volume(self) -> float:
"""Filter.action feedback 字段"""
return self.data.get("filtered_volume", 0.0)
@property
def current_status(self) -> str:
"""Filter.action feedback 字段"""
return self.data.get("current_status", "")
@property
def message(self) -> str:
return self.data.get("message", "")
@property
def max_temp(self) -> float:
return self._max_temp
@property
def max_stir_speed(self) -> float:
return self._max_stir_speed
@property
def max_volume(self) -> float:
return self._max_volume

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,210 @@
import asyncio
import logging
import time as time_module # 重命名time模块避免与参数冲突
from typing import Dict, Any
class VirtualHeatChill:
"""Virtual heat chill device for HeatChillProtocol testing"""
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')
# 设置默认值
self.device_id = device_id or "unknown_heatchill"
self.config = config or {}
self.logger = logging.getLogger(f"VirtualHeatChill.{self.device_id}")
self.data = {}
# 从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', 200.0)
self._min_temp = self.config.get('min_temp') or kwargs.get('min_temp', -80.0)
self._max_stir_speed = self.config.get('max_stir_speed') or kwargs.get('max_stir_speed', 1000.0)
# 处理其他kwargs参数
skip_keys = {'port', 'max_temp', 'min_temp', 'max_stir_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 heat chill"""
self.logger.info(f"Initializing virtual heat chill {self.device_id}")
# 初始化状态信息
self.data.update({
"status": "Idle",
"operation_mode": "Idle",
"is_stirring": False,
"stir_speed": 0.0,
"remaining_time": 0.0,
})
return True
async def cleanup(self) -> bool:
"""Cleanup virtual heat chill"""
self.logger.info(f"Cleaning up virtual heat chill {self.device_id}")
self.data.update({
"status": "Offline",
"operation_mode": "Offline",
"is_stirring": False,
"stir_speed": 0.0,
"remaining_time": 0.0
})
return True
async def heat_chill(self, vessel: str, temp: float, time: float, stir: bool,
stir_speed: float, purpose: str) -> bool:
"""Execute heat chill action - 按实际时间运行,实时更新剩余时间"""
self.logger.info(f"HeatChill: vessel={vessel}, temp={temp}°C, time={time}s, stir={stir}, stir_speed={stir_speed}")
# 验证参数
if temp > self._max_temp or temp < self._min_temp:
error_msg = f"温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C)"
self.logger.error(error_msg)
self.data.update({
"status": f"Error: {error_msg}",
"operation_mode": "Error"
})
return False
if stir and stir_speed > self._max_stir_speed:
error_msg = f"搅拌速度 {stir_speed} RPM 超出最大值 {self._max_stir_speed} RPM"
self.logger.error(error_msg)
self.data.update({
"status": f"Error: {error_msg}",
"operation_mode": "Error"
})
return False
# 确定操作模式
if temp > 25.0:
operation_mode = "Heating"
status_action = "加热"
elif temp < 25.0:
operation_mode = "Cooling"
status_action = "冷却"
else:
operation_mode = "Maintaining"
status_action = "保温"
# **修复**: 使用重命名的time模块
start_time = time_module.time()
total_time = time
# 开始操作
stir_info = f" | 搅拌: {stir_speed} RPM" if stir else ""
self.data.update({
"status": f"运行中: {status_action} {vessel}{temp}°C | 剩余: {total_time:.0f}s{stir_info}",
"operation_mode": operation_mode,
"is_stirring": stir,
"stir_speed": stir_speed if stir else 0.0,
"remaining_time": total_time,
})
# **修复**: 在等待过程中每秒更新剩余时间
while True:
current_time = time_module.time() # 使用重命名的time模块
elapsed = current_time - start_time
remaining = max(0, total_time - elapsed)
# 更新剩余时间和状态
self.data.update({
"remaining_time": remaining,
"status": f"运行中: {status_action} {vessel}{temp}°C | 剩余: {remaining:.0f}s{stir_info}"
})
# 如果时间到了,退出循环
if remaining <= 0:
break
# 等待1秒后再次检查
await asyncio.sleep(1.0)
# 操作完成
final_stir_info = f" | 搅拌: {stir_speed} RPM" if stir else ""
self.data.update({
"status": f"完成: {vessel} 已达到 {temp}°C | 用时: {total_time:.0f}s{final_stir_info}",
"operation_mode": "Completed",
"remaining_time": 0.0,
"is_stirring": False,
"stir_speed": 0.0
})
self.logger.info(f"HeatChill completed for vessel {vessel} at {temp}°C after {total_time}s")
return True
async def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> bool:
"""Start continuous heat chill"""
self.logger.info(f"HeatChillStart: vessel={vessel}, temp={temp}°C")
# 验证参数
if temp > self._max_temp or temp < self._min_temp:
error_msg = f"温度 {temp}°C 超出范围 ({self._min_temp}°C - {self._max_temp}°C)"
self.logger.error(error_msg)
self.data.update({
"status": f"Error: {error_msg}",
"operation_mode": "Error"
})
return False
# 确定操作模式
if temp > 25.0:
operation_mode = "Heating"
status_action = "持续加热"
elif temp < 25.0:
operation_mode = "Cooling"
status_action = "持续冷却"
else:
operation_mode = "Maintaining"
status_action = "恒温保持"
self.data.update({
"status": f"启动: {status_action} {vessel}{temp}°C | 持续运行",
"operation_mode": operation_mode,
"is_stirring": False,
"stir_speed": 0.0,
"remaining_time": -1.0, # -1 表示持续运行
})
return True
async def heat_chill_stop(self, vessel: str) -> bool:
"""Stop heat chill"""
self.logger.info(f"HeatChillStop: vessel={vessel}")
self.data.update({
"status": f"已停止: {vessel} 温控停止",
"operation_mode": "Stopped",
"is_stirring": False,
"stir_speed": 0.0,
"remaining_time": 0.0,
})
return True
# 状态属性
@property
def status(self) -> str:
return self.data.get("status", "Idle")
@property
def operation_mode(self) -> str:
return self.data.get("operation_mode", "Idle")
@property
def is_stirring(self) -> bool:
return self.data.get("is_stirring", False)
@property
def stir_speed(self) -> float:
return self.data.get("stir_speed", 0.0)
@property
def remaining_time(self) -> float:
return self.data.get("remaining_time", 0.0)

View File

@@ -0,0 +1,231 @@
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})"
def set_valve_position(self, command: Union[int, str]):
"""
设置阀门位置 - 兼容pump_protocol调用
这是set_position的别名方法用于兼容pump_protocol.py
Args:
command: 目标位置 (0-8) 或位置字符串
"""
return self.set_position(command)
# 使用示例
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,197 @@
import asyncio
import logging
from typing import Dict, Any, Optional
class VirtualPump:
"""Virtual pump device for transfer and cleaning operations"""
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')
# 设置默认值
self.device_id = device_id or "unknown_pump"
self.config = config or {}
self.logger = logging.getLogger(f"VirtualPump.{self.device_id}")
self.data = {}
# 从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', 10.0)
print(f"=== VirtualPump {self.device_id} created with max_volume={self._max_volume}, transfer_rate={self._transfer_rate} ===")
async def initialize(self) -> bool:
"""Initialize virtual pump"""
self.logger.info(f"Initializing virtual pump {self.device_id}")
self.data.update({
"status": "Idle",
"valve_position": 0,
"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"
})
return True
async def cleanup(self) -> bool:
"""Cleanup virtual pump"""
self.logger.info(f"Cleaning up virtual pump {self.device_id}")
return True
async def transfer(self, from_vessel: str, to_vessel: str, volume: float,
amount: str = "", time: float = 0.0, viscous: bool = False,
rinsing_solvent: str = "", rinsing_volume: float = 0.0,
rinsing_repeats: int = 0, solid: bool = False) -> bool:
"""Execute transfer operation"""
self.logger.info(f"Transferring {volume}mL from {from_vessel} to {to_vessel}")
# 计算转移时间
transfer_time = volume / self._transfer_rate if time == 0 else time
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
current_volume = step_volume * (i + 1)
self.data.update({
"progress": progress,
"transferred_volume": current_volume,
"current_status": f"Transferring: {progress:.1f}%"
})
self.logger.info(f"Transfer progress: {progress:.1f}%")
self.data.update({
"status": "Idle",
"current_status": "Transfer completed",
"progress": 100.0,
"transferred_volume": volume
})
return True
async def clean_vessel(self, vessel: str, solvent: str, volume: float,
temp: float, repeats: int = 1) -> bool:
"""Execute vessel cleaning operation - matches CleanVessel action"""
self.logger.info(f"Starting vessel cleaning: {vessel} with {solvent} ({volume}mL at {temp}°C, {repeats} repeats)")
# 更新设备状态
self.data.update({
"status": "Running",
"from_vessel": f"flask_{solvent}",
"to_vessel": vessel,
"current_status": "Cleaning in progress",
"progress": 0.0,
"transferred_volume": 0.0
})
# 计算清洗时间(基于体积和重复次数)
# 假设清洗速度为 transfer_rate 的一半(因为需要加载和排放)
cleaning_rate = self._transfer_rate / 2
cleaning_time_per_cycle = volume / cleaning_rate
total_cleaning_time = cleaning_time_per_cycle * repeats
# 模拟清洗过程
steps_per_repeat = 10 # 每次重复清洗分10个步骤
total_steps = steps_per_repeat * repeats
step_time = total_cleaning_time / total_steps
for repeat in range(repeats):
self.logger.info(f"Starting cleaning cycle {repeat + 1}/{repeats}")
for step in range(steps_per_repeat):
await asyncio.sleep(step_time)
# 计算当前进度
current_step = repeat * steps_per_repeat + step + 1
progress = (current_step / total_steps) * 100
# 计算已处理的体积
volume_processed = (current_step / total_steps) * volume * repeats
# 更新状态
self.data.update({
"progress": progress,
"transferred_volume": volume_processed,
"current_status": f"Cleaning cycle {repeat + 1}/{repeats} - Step {step + 1}/{steps_per_repeat} ({progress:.1f}%)"
})
self.logger.info(f"Cleaning progress: {progress:.1f}% (Cycle {repeat + 1}/{repeats})")
# 清洗完成
self.data.update({
"status": "Idle",
"current_status": "Cleaning completed successfully",
"progress": 100.0,
"transferred_volume": volume * repeats,
"from_vessel": "",
"to_vessel": ""
})
self.logger.info(f"Vessel cleaning completed: {vessel}")
return True
# 状态属性
@property
def status(self) -> str:
return self.data.get("status", "Unknown")
@property
def valve_position(self) -> int:
return self.data.get("valve_position", 0)
@property
def current_volume(self) -> float:
return self.data.get("current_volume", 0.0)
@property
def max_volume(self) -> float:
return self.data.get("max_volume", 0.0)
@property
def transfer_rate(self) -> float:
return self.data.get("transfer_rate", 0.0)
@property
def from_vessel(self) -> str:
return self.data.get("from_vessel", "")
@property
def to_vessel(self) -> str:
return self.data.get("to_vessel", "")
@property
def progress(self) -> float:
return self.data.get("progress", 0.0)
@property
def transferred_volume(self) -> float:
return self.data.get("transferred_volume", 0.0)
@property
def current_status(self) -> str:
return self.data.get("current_status", "Ready")

View File

@@ -0,0 +1,228 @@
import asyncio
import logging
import time as time_module
from typing import Dict, Any, Optional
class VirtualRotavap:
"""Virtual rotary evaporator device - 简化版,只保留核心功能"""
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 = {}
# 从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"""
self.logger.info(f"Initializing virtual rotary evaporator {self.device_id}")
# 只保留核心状态
self.data.update({
"status": "Idle",
"rotavap_state": "Ready", # Ready, Evaporating, Completed, Error
"current_temp": 25.0,
"target_temp": 25.0,
"rotation_speed": 0.0,
"vacuum_pressure": 1.0, # 大气压
"evaporated_volume": 0.0,
"progress": 0.0,
"remaining_time": 0.0,
"message": "Ready for evaporation"
})
return True
async def cleanup(self) -> bool:
"""Cleanup virtual rotary evaporator"""
self.logger.info(f"Cleaning up virtual rotary evaporator {self.device_id}")
self.data.update({
"status": "Offline",
"rotavap_state": "Offline",
"current_temp": 25.0,
"rotation_speed": 0.0,
"vacuum_pressure": 1.0,
"message": "System offline"
})
return True
async def evaporate(
self,
vessel: str,
pressure: float = 0.1,
temp: float = 60.0,
time: float = 1800.0, # 30分钟默认
stir_speed: float = 100.0
) -> bool:
"""Execute evaporate action - 简化的蒸发流程"""
self.logger.info(f"Evaporate: vessel={vessel}, pressure={pressure} bar, temp={temp}°C, time={time}s, rotation={stir_speed} RPM")
# 验证参数
if temp > self._max_temp or temp < 10.0:
error_msg = f"温度 {temp}°C 超出范围 (10-{self._max_temp}°C)"
self.logger.error(error_msg)
self.data.update({
"status": f"Error: {error_msg}",
"rotavap_state": "Error",
"message": error_msg
})
return False
if stir_speed > self._max_rotation_speed or stir_speed < 10.0:
error_msg = f"旋转速度 {stir_speed} RPM 超出范围 (10-{self._max_rotation_speed} RPM)"
self.logger.error(error_msg)
self.data.update({
"status": f"Error: {error_msg}",
"rotavap_state": "Error",
"message": error_msg
})
return False
if pressure < 0.01 or pressure > 1.0:
error_msg = f"真空度 {pressure} bar 超出范围 (0.01-1.0 bar)"
self.logger.error(error_msg)
self.data.update({
"status": f"Error: {error_msg}",
"rotavap_state": "Error",
"message": error_msg
})
return False
# 开始蒸发
self.data.update({
"status": f"蒸发中: {vessel}",
"rotavap_state": "Evaporating",
"current_temp": temp,
"target_temp": temp,
"rotation_speed": stir_speed,
"vacuum_pressure": pressure,
"remaining_time": time,
"progress": 0.0,
"evaporated_volume": 0.0,
"message": f"Evaporating {vessel} at {temp}°C, {pressure} bar, {stir_speed} RPM"
})
try:
# 蒸发过程 - 实时更新进度
start_time = time_module.time()
total_time = time
while True:
current_time = time_module.time()
elapsed = current_time - start_time
remaining = max(0, total_time - elapsed)
progress = min(100.0, (elapsed / total_time) * 100)
# 模拟蒸发体积
evaporated_vol = progress * 0.8 # 假设最多蒸发80mL
# 更新状态
self.data.update({
"remaining_time": remaining,
"progress": progress,
"evaporated_volume": evaporated_vol,
"status": f"蒸发中: {vessel} | {temp}°C | {pressure} bar | {progress:.1f}% | 剩余: {remaining:.0f}s",
"message": f"Evaporating: {progress:.1f}% complete, {remaining:.0f}s remaining"
})
# 时间到了,退出循环
if remaining <= 0:
break
# 每秒更新一次
await asyncio.sleep(1.0)
# 蒸发完成
final_evaporated = 80.0
self.data.update({
"status": f"蒸发完成: {vessel} | 蒸发量: {final_evaporated:.1f}mL",
"rotavap_state": "Completed",
"evaporated_volume": final_evaporated,
"progress": 100.0,
"remaining_time": 0.0,
"current_temp": 25.0, # 冷却下来
"rotation_speed": 0.0, # 停止旋转
"vacuum_pressure": 1.0, # 恢复大气压
"message": f"Evaporation completed: {final_evaporated}mL evaporated from {vessel}"
})
self.logger.info(f"Evaporation completed: {final_evaporated}mL evaporated from {vessel}")
return True
except Exception as e:
# 出错处理
self.logger.error(f"Error during evaporation: {str(e)}")
self.data.update({
"status": f"蒸发错误: {str(e)}",
"rotavap_state": "Error",
"current_temp": 25.0,
"rotation_speed": 0.0,
"vacuum_pressure": 1.0,
"message": f"Evaporation failed: {str(e)}"
})
return False
# === 核心状态属性 ===
@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 rotation_speed(self) -> float:
return self.data.get("rotation_speed", 0.0)
@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", "")
@property
def max_temp(self) -> float:
return self._max_temp
@property
def max_rotation_speed(self) -> float:
return self._max_rotation_speed
@property
def remaining_time(self) -> float:
return self.data.get("remaining_time", 0.0)

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,147 @@
import time
import asyncio
from typing import Union
class VirtualSolenoidValve:
"""
虚拟电磁阀门 - 简单的开关型阀门,只有开启和关闭两个状态
"""
def __init__(self, device_id: str = None, config: dict = None, **kwargs):
# 从配置中获取参数,提供默认值
if config is None:
config = {}
self.device_id = device_id
self.port = config.get("port", "VIRTUAL")
self.voltage = config.get("voltage", 12.0)
self.response_time = config.get("response_time", 0.1)
# 状态属性
self._status = "Idle"
self._valve_state = "Closed" # "Open" or "Closed"
self._is_open = False
async def initialize(self) -> bool:
"""初始化设备"""
self._status = "Idle"
return True
async def cleanup(self) -> bool:
"""清理资源"""
return True
@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"
async def set_valve_position(self, command: str = None, **kwargs):
"""
设置阀门位置 - ROS动作接口
Args:
command: "OPEN"/"CLOSED" 或其他控制命令
"""
if command is None:
return {"success": False, "message": "Missing command parameter"}
print(f"SOLENOID_VALVE: {self.device_id} 接收到命令: {command}")
self._status = "Busy"
# 模拟阀门响应时间
await asyncio.sleep(self.response_time)
# 处理不同的命令格式
if isinstance(command, str):
cmd_upper = command.upper()
if cmd_upper in ["OPEN", "ON", "TRUE", "1"]:
self._is_open = True
self._valve_state = "Open"
result_msg = f"Valve {self.device_id} opened"
elif cmd_upper in ["CLOSED", "CLOSE", "OFF", "FALSE", "0"]:
self._is_open = False
self._valve_state = "Closed"
result_msg = f"Valve {self.device_id} closed"
else:
# 可能是端口名称,处理路径设置
# 对于简单电磁阀,任何非关闭命令都视为开启
self._is_open = True
self._valve_state = "Open"
result_msg = f"Valve {self.device_id} set to position: {command}"
else:
self._status = "Error"
return {"success": False, "message": "Invalid command type"}
self._status = "Idle"
print(f"SOLENOID_VALVE: {result_msg}")
return {
"success": True,
"message": result_msg,
"valve_position": self.get_valve_position()
}
async def open(self, **kwargs):
"""打开电磁阀 - ROS动作接口"""
return await self.set_valve_position(command="OPEN")
async def close(self, **kwargs):
"""关闭电磁阀 - ROS动作接口"""
return await self.set_valve_position(command="CLOSED")
async def set_state(self, command: Union[bool, str], **kwargs):
"""
设置阀门状态 - 兼容 SendCmd 类型
Args:
command: True/False 或 "open"/"close"
"""
if isinstance(command, bool):
cmd_str = "OPEN" if command else "CLOSED"
elif isinstance(command, str):
cmd_str = command
else:
return {"success": False, "message": "Invalid command type"}
return await self.set_valve_position(command=cmd_str)
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 {
"device_id": self.device_id,
"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()
}
async def reset(self):
"""重置阀门到关闭状态"""
return await self.close()

View File

@@ -0,0 +1,215 @@
import asyncio
import logging
import time as time_module
from typing import Dict, Any
class VirtualStirrer:
"""Virtual stirrer device for StirProtocol testing - 功能完整版"""
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')
# 设置默认值
self.device_id = device_id or "unknown_stirrer"
self.config = config or {}
self.logger = logging.getLogger(f"VirtualStirrer.{self.device_id}")
self.data = {}
# 从config或kwargs中获取配置参数
self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL')
self._max_speed = self.config.get('max_speed') or kwargs.get('max_speed', 1500.0)
self._min_speed = self.config.get('min_speed') or kwargs.get('min_speed', 50.0)
# 处理其他kwargs参数
skip_keys = {'port', 'max_speed', 'min_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 stirrer"""
self.logger.info(f"Initializing virtual stirrer {self.device_id}")
# 初始化状态信息
self.data.update({
"status": "Idle",
"operation_mode": "Idle", # 操作模式: Idle, Stirring, Settling, Completed, Error
"current_vessel": "", # 当前搅拌的容器
"current_speed": 0.0, # 当前搅拌速度
"is_stirring": False, # 是否正在搅拌
"remaining_time": 0.0, # 剩余时间
})
return True
async def cleanup(self) -> bool:
"""Cleanup virtual stirrer"""
self.logger.info(f"Cleaning up virtual stirrer {self.device_id}")
self.data.update({
"status": "Offline",
"operation_mode": "Offline",
"current_vessel": "",
"current_speed": 0.0,
"is_stirring": False,
"remaining_time": 0.0,
})
return True
async def stir(self, stir_time: float, stir_speed: float, settling_time: float) -> bool:
"""Execute stir action - 定时搅拌 + 沉降"""
self.logger.info(f"Stir: speed={stir_speed} RPM, time={stir_time}s, settling={settling_time}s")
# 验证参数
if stir_speed > self._max_speed or stir_speed < self._min_speed:
error_msg = f"搅拌速度 {stir_speed} RPM 超出范围 ({self._min_speed} - {self._max_speed} RPM)"
self.logger.error(error_msg)
self.data.update({
"status": f"Error: {error_msg}",
"operation_mode": "Error"
})
return False
# === 第一阶段:搅拌 ===
start_time = time_module.time()
total_stir_time = stir_time
self.data.update({
"status": f"搅拌中: {stir_speed} RPM | 剩余: {total_stir_time:.0f}s",
"operation_mode": "Stirring",
"current_speed": stir_speed,
"is_stirring": True,
"remaining_time": total_stir_time,
})
# 搅拌过程 - 实时更新剩余时间
while True:
current_time = time_module.time()
elapsed = current_time - start_time
remaining = max(0, total_stir_time - elapsed)
# 更新状态
self.data.update({
"remaining_time": remaining,
"status": f"搅拌中: {stir_speed} RPM | 剩余: {remaining:.0f}s"
})
# 搅拌时间到了
if remaining <= 0:
break
await asyncio.sleep(1.0)
# === 第二阶段:沉降(如果需要)===
if settling_time > 0:
start_settling_time = time_module.time()
total_settling_time = settling_time
self.data.update({
"status": f"沉降中: 停止搅拌 | 剩余: {total_settling_time:.0f}s",
"operation_mode": "Settling",
"current_speed": 0.0,
"is_stirring": False,
"remaining_time": total_settling_time,
})
# 沉降过程 - 实时更新剩余时间
while True:
current_time = time_module.time()
elapsed = current_time - start_settling_time
remaining = max(0, total_settling_time - elapsed)
# 更新状态
self.data.update({
"remaining_time": remaining,
"status": f"沉降中: 停止搅拌 | 剩余: {remaining:.0f}s"
})
# 沉降时间到了
if remaining <= 0:
break
await asyncio.sleep(1.0)
# === 操作完成 ===
settling_info = f" | 沉降: {settling_time:.0f}s" if settling_time > 0 else ""
self.data.update({
"status": f"完成: 搅拌 {stir_speed} RPM, {stir_time:.0f}s{settling_info}",
"operation_mode": "Completed",
"current_speed": 0.0,
"is_stirring": False,
"remaining_time": 0.0,
})
self.logger.info(f"Stir completed: {stir_speed} RPM for {stir_time}s + settling {settling_time}s")
return True
async def start_stir(self, vessel: str, stir_speed: float, purpose: str) -> bool:
"""Start stir action - 开始持续搅拌"""
self.logger.info(f"StartStir: vessel={vessel}, speed={stir_speed} RPM, purpose={purpose}")
# 验证参数
if stir_speed > self._max_speed or stir_speed < self._min_speed:
error_msg = f"搅拌速度 {stir_speed} RPM 超出范围 ({self._min_speed} - {self._max_speed} RPM)"
self.logger.error(error_msg)
self.data.update({
"status": f"Error: {error_msg}",
"operation_mode": "Error"
})
return False
self.data.update({
"status": f"启动: 持续搅拌 {vessel} at {stir_speed} RPM | {purpose}",
"operation_mode": "Stirring",
"current_vessel": vessel,
"current_speed": stir_speed,
"is_stirring": True,
"remaining_time": -1.0, # -1 表示持续运行
})
return True
async def stop_stir(self, vessel: str) -> bool:
"""Stop stir action - 停止搅拌"""
self.logger.info(f"StopStir: vessel={vessel}")
current_speed = self.data.get("current_speed", 0.0)
self.data.update({
"status": f"已停止: {vessel} 搅拌停止 | 之前速度: {current_speed} RPM",
"operation_mode": "Stopped",
"current_vessel": "",
"current_speed": 0.0,
"is_stirring": False,
"remaining_time": 0.0,
})
return True
# 状态属性
@property
def status(self) -> str:
return self.data.get("status", "Idle")
@property
def operation_mode(self) -> str:
return self.data.get("operation_mode", "Idle")
@property
def current_vessel(self) -> str:
return self.data.get("current_vessel", "")
@property
def current_speed(self) -> float:
return self.data.get("current_speed", 0.0)
@property
def is_stirring(self) -> bool:
return self.data.get("is_stirring", False)
@property
def remaining_time(self) -> float:
return self.data.get("remaining_time", 0.0)

View File

@@ -0,0 +1,328 @@
import asyncio
import time
from enum import Enum
from typing import Union, Optional
import logging
class VirtualPumpMode(Enum):
Normal = 0
AccuratePos = 1
AccuratePosVel = 2
class VirtualTransferPump:
"""虚拟转移泵类 - 模拟泵的基本功能,无需实际硬件"""
def __init__(self, device_id: str = None, config: dict = None, **kwargs):
"""
初始化虚拟转移泵
Args:
device_id: 设备ID
config: 配置字典包含max_volume, port等参数
**kwargs: 其他参数,确保兼容性
"""
self.device_id = device_id or "virtual_transfer_pump"
# 从config或kwargs中获取参数确保类型正确
if config:
self.max_volume = float(config.get('max_volume', 25.0))
self.port = config.get('port', 'VIRTUAL')
else:
self.max_volume = float(kwargs.get('max_volume', 25.0))
self.port = kwargs.get('port', 'VIRTUAL')
self._transfer_rate = float(kwargs.get('transfer_rate', 0))
self.mode = kwargs.get('mode', VirtualPumpMode.Normal)
# 状态变量 - 确保都是正确类型
self._status = "Idle"
self._position = 0.0 # float
self._max_velocity = 5.0 # float
self._current_volume = 0.0 # float
self.logger = logging.getLogger(f"VirtualTransferPump.{self.device_id}")
async def initialize(self) -> bool:
"""初始化虚拟泵"""
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:
"""清理虚拟泵"""
self.logger.info(f"Cleaning up virtual pump {self.device_id}")
self._status = "Idle"
return True
# 基本属性
@property
def status(self) -> str:
return self._status
@property
def position(self) -> float:
"""当前柱塞位置 (ml)"""
return self._position
@property
def current_volume(self) -> float:
"""当前注射器中的体积 (ml)"""
return self._current_volume
@property
def max_velocity(self) -> float:
return self._max_velocity
@property
def transfer_rate(self) -> float:
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")
def get_status(self) -> str:
"""获取泵状态"""
return self._status
async def _simulate_operation(self, duration: float):
"""模拟操作延时"""
self._status = "Busy"
await asyncio.sleep(duration)
self._status = "Idle"
def _calculate_duration(self, volume: float, velocity: float = None) -> float:
"""计算操作持续时间"""
if velocity is None:
velocity = self._max_velocity
return abs(volume) / velocity
# 新的set_position方法 - 专门用于SetPumpPosition动作
async def set_position(self, position: float, max_velocity: float = None):
"""
移动到绝对位置 - 专门用于SetPumpPosition动作
Args:
position (float): 目标位置 (ml)
max_velocity (float): 移动速度 (ml/s)
Returns:
dict: 符合SetPumpPosition.action定义的结果
"""
try:
# 验证并转换参数
target_position = float(position)
velocity = float(max_velocity) if max_velocity is not None else self._max_velocity
# 限制位置在有效范围内
target_position = max(0.0, min(float(self.max_volume), target_position))
# 计算移动距离和时间
volume_to_move = abs(target_position - self._position)
duration = self._calculate_duration(volume_to_move, velocity)
self.logger.info(f"SET_POSITION: Moving to {target_position} ml (current: {self._position} ml), velocity: {velocity} ml/s")
# 模拟移动过程
start_position = self._position
steps = 10 if duration > 0.1 else 1 # 如果移动距离很小只用1步
step_duration = duration / steps if steps > 1 else duration
for i in range(steps + 1):
# 计算当前位置和进度
progress = (i / steps) * 100 if steps > 0 else 100
current_pos = start_position + (target_position - start_position) * (i / steps) if steps > 0 else target_position
# 更新状态
self._status = "Moving" if i < steps else "Idle"
self._position = current_pos
self._current_volume = current_pos
# 等待一小步时间
if i < steps and step_duration > 0:
await asyncio.sleep(step_duration)
# 确保最终位置准确
self._position = target_position
self._current_volume = target_position
self._status = "Idle"
self.logger.info(f"SET_POSITION: Reached position {self._position} ml, current volume: {self._current_volume} ml")
# 返回符合action定义的结果
return {
"success": True,
"message": f"Successfully moved to position {self._position} ml"
}
except Exception as e:
error_msg = f"Failed to set position: {str(e)}"
self.logger.error(error_msg)
return {
"success": False,
"message": error_msg
}
# 其他泵操作方法
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):
"""吸液操作"""
await self.pull_plunger(volume, velocity)
async def dispense(self, volume: float, velocity: float = None):
"""排液操作"""
await self.push_plunger(volume, velocity)
async def transfer(self, volume: float, aspirate_velocity: float = None, dispense_velocity: float = None):
"""转移操作(先吸后排)"""
# 吸液
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"VirtualTransferPump({self.device_id}: {self._current_volume:.2f}/{self.max_volume} ml, {self._status})"
def __repr__(self):
return self.__str__()
# 使用示例
async def demo():
"""虚拟泵使用示例"""
pump = VirtualTransferPump("demo_pump", {"max_volume": 50.0})
await pump.initialize()
print(f"Initial state: {pump}")
# 测试set_position方法
result = await pump.set_position(10.0, max_velocity=2.0)
print(f"Set position result: {result}")
print(f"After setting position to 10ml: {pump}")
# 吸液测试
await pump.aspirate(5.0, velocity=2.0)
print(f"After aspirating 5ml: {pump}")
# 清空测试
result = await pump.set_position(0.0)
print(f"Empty result: {result}")
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

@@ -0,0 +1,105 @@
import asyncio
import logging
from typing import Dict, Any
class VirtualValve:
"""Virtual valve device for AddProtocol testing"""
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')
# 设置默认值
self.device_id = device_id or "unknown_valve"
self.config = config or {}
self.logger = logging.getLogger(f"VirtualValve.{self.device_id}")
self.data = {}
print(f"=== VirtualValve {self.device_id} is being created! ===")
print(f"=== Config: {self.config} ===")
print(f"=== Kwargs: {kwargs} ===")
# 处理所有配置参数包括port
self.port = self.config.get('port', 'VIRTUAL')
self.positions = self.config.get('positions', 6)
self.current_position = 0
# 忽略其他可能的kwargs参数
for key, value in kwargs.items():
if not hasattr(self, key):
setattr(self, key, value)
async def initialize(self) -> bool:
"""Initialize virtual valve"""
print(f"=== VirtualValve {self.device_id} initialize() called! ===")
self.logger.info(f"Initializing virtual valve {self.device_id}")
self.data.update({
"status": "Idle",
"valve_state": "Closed",
"current_position": 0,
"target_position": 0,
"max_positions": self.positions
})
return True
async def cleanup(self) -> bool:
"""Cleanup virtual valve"""
self.logger.info(f"Cleaning up virtual valve {self.device_id}")
return True
async def set_position(self, position: int) -> bool:
"""Set valve position - matches SendCmd action"""
if 0 <= position <= self.positions:
self.logger.info(f"Setting valve position to {position}")
self.data.update({
"target_position": position,
"current_position": position,
"valve_state": "Open" if position > 0 else "Closed"
})
return True
else:
self.logger.error(f"Invalid position {position}. Must be 0-{self.positions}")
return False
async def open(self) -> bool:
"""Open valve - matches EmptyIn action"""
self.logger.info("Opening valve")
self.data.update({
"valve_state": "Open",
"current_position": 1
})
return True
async def close(self) -> bool:
"""Close valve - matches EmptyIn action"""
self.logger.info("Closing valve")
self.data.update({
"valve_state": "Closed",
"current_position": 0
})
return True
# 状态属性
@property
def status(self) -> str:
return self.data.get("status", "Unknown")
@property
def valve_state(self) -> str:
return self.data.get("valve_state", "Unknown")
@property
def current_position(self) -> int:
return self.data.get("current_position", 0)
@property
def target_position(self) -> int:
return self.data.get("target_position", 0)
@property
def max_positions(self) -> int:
return self.data.get("max_positions", 6)

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