mirror of
https://github.com/dptech-corp/Uni-Lab-OS.git
synced 2026-02-04 13:25:13 +00:00
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 commit498c997ad7. * Reapply "修改物料跟随与物料添加逻辑" This reverts commit3a60d2ae81. * Revert "Merge remote-tracking branch 'upstream/dev' into device_visualization" This reverts commitfa727220af, reversing changes made to498c997ad7. * 修改物料放下时的方法,如果选择 修改物料放下时的方法, 如果选择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 commit56d45b94f5. * Reapply "修改物体attach时,多次赋值当前时间导致卡顿问题," This reverts commit07d9db20c3. * 添加缺少物料:"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 commit5d9953c3e5. * Reapply "change to debug level" This reverts commit2487bb6ffc. * 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 commit498c997ad7. * Reapply "修改物料跟随与物料添加逻辑" This reverts commit3a60d2ae81. * Revert "Merge remote-tracking branch 'upstream/dev' into device_visualization" This reverts commitfa727220af, reversing changes made to498c997ad7. * 修改物料放下时的方法,如果选择 修改物料放下时的方法, 如果选择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 commit56d45b94f5. * Reapply "修改物体attach时,多次赋值当前时间导致卡顿问题," This reverts commit07d9db20c3. * 添加缺少物料:"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 commit5d9953c3e5. * Reapply "change to debug level" This reverts commit2487bb6ffc. * 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 commit498c997ad7. * Reapply "修改物料跟随与物料添加逻辑" This reverts commit3a60d2ae81. * Revert "Merge remote-tracking branch 'upstream/dev' into device_visualization" This reverts commitfa727220af, reversing changes made to498c997ad7. * 修改物料放下时的方法,如果选择 修改物料放下时的方法, 如果选择drop_trash,则删除物料显示 如果选择drop,则让其解除连接 * unilab添加moveit启动 1,整合所有moveit节点到一个move_group中,并整合所有的controller依次激活 2,添加pymoveit2的节点,使用json可直接启动 3,修改机械臂规划方式,添加约束,让冗余关节不会进行过多移动 * 修改物体attach时,多次赋值当前时间导致卡顿问题, * Revert "修改物体attach时,多次赋值当前时间导致卡顿问题," This reverts commit56d45b94f5. * Reapply "修改物体attach时,多次赋值当前时间导致卡顿问题," This reverts commit07d9db20c3. * 添加缺少物料:"plate_well_G12", * add * fix tip resource data * liquid states * change to debug level * Revert "change to debug level" This reverts commit5d9953c3e5. * Reapply "change to debug level" This reverts commit2487bb6ffc. * 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>
This commit is contained in:
1098
unilabos/devices/liquid_handling/biomek.py
Normal file
1098
unilabos/devices/liquid_handling/biomek.py
Normal file
File diff suppressed because it is too large
Load Diff
642
unilabos/devices/liquid_handling/biomek.txt
Normal file
642
unilabos/devices/liquid_handling/biomek.txt
Normal file
File diff suppressed because one or more lines are too long
2697
unilabos/devices/liquid_handling/biomek_temporary_protocol.json
Normal file
2697
unilabos/devices/liquid_handling/biomek_temporary_protocol.json
Normal file
File diff suppressed because it is too large
Load Diff
1006
unilabos/devices/liquid_handling/biomek_test.py
Normal file
1006
unilabos/devices/liquid_handling/biomek_test.py
Normal file
File diff suppressed because it is too large
Load Diff
3760
unilabos/devices/liquid_handling/complete_biomek_protocol.json
Normal file
3760
unilabos/devices/liquid_handling/complete_biomek_protocol.json
Normal file
File diff suppressed because it is too large
Load Diff
4201
unilabos/devices/liquid_handling/complete_biomek_protocol_0608.json
Normal file
4201
unilabos/devices/liquid_handling/complete_biomek_protocol_0608.json
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -6,13 +6,8 @@ import asyncio
|
||||
import time
|
||||
|
||||
from pylabrobot.liquid_handling import LiquidHandler
|
||||
from pylabrobot.resources import (
|
||||
Resource,
|
||||
TipRack,
|
||||
Container,
|
||||
Coordinate,
|
||||
Well
|
||||
)
|
||||
from pylabrobot.resources import Resource, TipRack, Container, Coordinate, Well
|
||||
|
||||
|
||||
class LiquidHandlerAbstract(LiquidHandler):
|
||||
"""Extended LiquidHandler with additional operations."""
|
||||
@@ -21,6 +16,19 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
# REMOVE LIQUID --------------------------------------------------
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
async def create_protocol(
|
||||
self,
|
||||
protocol_name: str,
|
||||
protocol_description: str,
|
||||
protocol_version: str,
|
||||
protocol_author: str,
|
||||
protocol_date: str,
|
||||
protocol_type: str,
|
||||
none_keys: List[str] = [],
|
||||
):
|
||||
"""Create a new protocol with the given metadata."""
|
||||
pass
|
||||
|
||||
async def remove_liquid(
|
||||
self,
|
||||
vols: List[float],
|
||||
@@ -35,26 +43,26 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
spread: Optional[Literal["wide", "tight", "custom"]] = "wide",
|
||||
delays: Optional[List[int]] = None,
|
||||
is_96_well: Optional[bool] = False,
|
||||
top: Optional[List(float)] = None,
|
||||
none_keys: List[str] = []
|
||||
top: Optional[List[float]] = None,
|
||||
none_keys: List[str] = [],
|
||||
):
|
||||
"""A complete *remove* (aspirate → waste) operation."""
|
||||
trash = self.deck.get_trash_area()
|
||||
try:
|
||||
if is_96_well:
|
||||
pass # This mode is not verified
|
||||
pass # This mode is not verified
|
||||
else:
|
||||
if len(vols) != len(sources):
|
||||
raise ValueError("Length of `vols` must match `sources`.")
|
||||
|
||||
for src, vol in zip(sources, vols):
|
||||
self.move_to(src, dis_to_top=top[0] if top else 0)
|
||||
await self.move_to(src, dis_to_top=top[0] if top else 0)
|
||||
tip = next(self.current_tip)
|
||||
await self.pick_up_tips(tip)
|
||||
await self.aspirate(
|
||||
resources=[src],
|
||||
vols=[vol],
|
||||
use_channels=use_channels, # only aspirate96 used, default to None
|
||||
use_channels=use_channels, # only aspirate96 used, default to None
|
||||
flow_rates=[flow_rates[0]] if flow_rates else None,
|
||||
offsets=[offsets[0]] if offsets else None,
|
||||
liquid_height=[liquid_height[0]] if liquid_height else None,
|
||||
@@ -64,15 +72,15 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
await self.custom_delay(seconds=delays[0] if delays else 0)
|
||||
await self.dispense(
|
||||
resources=waste_liquid,
|
||||
vols=[vol],
|
||||
use_channels=use_channels,
|
||||
flow_rates=[flow_rates[1]] if flow_rates else None,
|
||||
offsets=[offsets[1]] if offsets else None,
|
||||
liquid_height=[liquid_height[1]] if liquid_height else None,
|
||||
blow_out_air_volume=blow_out_air_volume[1] if blow_out_air_volume else None,
|
||||
spread=spread,
|
||||
)
|
||||
await self.discard_tips() # For now, each of tips is discarded after use
|
||||
vols=[vol],
|
||||
use_channels=use_channels,
|
||||
flow_rates=[flow_rates[1]] if flow_rates else None,
|
||||
offsets=[offsets[1]] if offsets else None,
|
||||
liquid_height=[liquid_height[1]] if liquid_height else None,
|
||||
blow_out_air_volume=blow_out_air_volume[1] if blow_out_air_volume else None,
|
||||
spread=spread,
|
||||
)
|
||||
await self.discard_tips() # For now, each of tips is discarded after use
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Liquid removal failed: {e}") from e
|
||||
@@ -100,13 +108,13 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
mix_vol: Optional[int] = None,
|
||||
mix_rate: Optional[int] = None,
|
||||
mix_liquid_height: Optional[float] = None,
|
||||
none_keys: List[str] = []
|
||||
none_keys: List[str] = [],
|
||||
):
|
||||
"""A complete *add* (aspirate reagent → dispense into targets) operation."""
|
||||
|
||||
try:
|
||||
if is_96_well:
|
||||
pass # This mode is not verified.
|
||||
pass # This mode is not verified.
|
||||
else:
|
||||
if len(asp_vols) != len(targets):
|
||||
raise ValueError("Length of `vols` must match `targets`.")
|
||||
@@ -122,7 +130,7 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
offsets=[offsets[0]] if offsets else None,
|
||||
liquid_height=[liquid_height[0]] if liquid_height else None,
|
||||
blow_out_air_volume=[blow_out_air_volume[0]] if blow_out_air_volume else None,
|
||||
spread=spread
|
||||
spread=spread,
|
||||
)
|
||||
if delays is not None:
|
||||
await self.custom_delay(seconds=delays[0])
|
||||
@@ -144,7 +152,8 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
mix_vol=mix_vol,
|
||||
offsets=offsets if offsets else None,
|
||||
height_to_bottom=mix_liquid_height if mix_liquid_height else None,
|
||||
mix_rate=mix_rate if mix_rate else None)
|
||||
mix_rate=mix_rate if mix_rate else None,
|
||||
)
|
||||
if delays is not None:
|
||||
await self.custom_delay(seconds=delays[1])
|
||||
await self.touch_tip(targets[_])
|
||||
@@ -158,13 +167,13 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
# ---------------------------------------------------------------
|
||||
async def transfer_liquid(
|
||||
self,
|
||||
asp_vols: Union[List[float], float],
|
||||
dis_vols: Union[List[float], float],
|
||||
sources: Sequence[Container],
|
||||
targets: Sequence[Container],
|
||||
tip_racks: Sequence[TipRack],
|
||||
*,
|
||||
use_channels: Optional[List[int]] = None,
|
||||
asp_vols: Union[List[float], float],
|
||||
dis_vols: Union[List[float], float],
|
||||
asp_flow_rates: Optional[List[Optional[float]]] = None,
|
||||
dis_flow_rates: Optional[List[Optional[float]]] = None,
|
||||
offsets: Optional[List[Coordinate]] = None,
|
||||
@@ -179,7 +188,7 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
mix_rate: Optional[int] = None,
|
||||
mix_liquid_height: Optional[float] = None,
|
||||
delays: Optional[List[int]] = None,
|
||||
none_keys: List[str] = []
|
||||
none_keys: List[str] = [],
|
||||
):
|
||||
"""Transfer liquid from each *source* well/plate to the corresponding *target*.
|
||||
|
||||
@@ -201,14 +210,15 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
# 96‑channel head mode
|
||||
# ------------------------------------------------------------------
|
||||
if is_96_well:
|
||||
pass # This mode is not verified
|
||||
pass # This mode is not verified
|
||||
else:
|
||||
if not (len(asp_vols) == len(sources) and len(dis_vols) == len(targets)):
|
||||
raise ValueError("`sources`, `targets`, and `vols` must have the same length.")
|
||||
|
||||
tip_iter = self.iter_tips(tip_racks)
|
||||
for src, tgt, asp_vol, asp_flow_rate, dis_vol, dis_flow_rate in (
|
||||
zip(sources, targets, asp_vols, asp_flow_rates, dis_vols, dis_flow_rates)):
|
||||
for src, tgt, asp_vol, asp_flow_rate, dis_vol, dis_flow_rate in zip(
|
||||
sources, targets, asp_vols, asp_flow_rates, dis_vols, dis_flow_rates
|
||||
):
|
||||
tip = next(tip_iter)
|
||||
await self.pick_up_tips(tip)
|
||||
# Aspirate from source
|
||||
@@ -247,9 +257,9 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
except Exception as exc:
|
||||
raise RuntimeError(f"Liquid transfer failed: {exc}") from exc
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# Helper utilities
|
||||
# ---------------------------------------------------------------
|
||||
# ---------------------------------------------------------------
|
||||
# Helper utilities
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
async def custom_delay(self, seconds=0, msg=None):
|
||||
"""
|
||||
@@ -266,28 +276,26 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
print(f"Done: {msg}")
|
||||
print(f"Current time: {time.strftime('%H:%M:%S')}")
|
||||
|
||||
async def touch_tip(self,
|
||||
targets: Sequence[Container],
|
||||
):
|
||||
async def touch_tip(self, targets: Sequence[Container]):
|
||||
"""Touch the tip to the side of the well."""
|
||||
await self.aspirate(
|
||||
resources=[targets],
|
||||
vols=[0],
|
||||
use_channels=None,
|
||||
flow_rates=None,
|
||||
offsets=[Coordinate(x=-targets.get_size_x()/2,y=0,z=0)],
|
||||
offsets=[Coordinate(x=-targets.get_size_x() / 2, y=0, z=0)],
|
||||
liquid_height=None,
|
||||
blow_out_air_volume=None
|
||||
blow_out_air_volume=None,
|
||||
)
|
||||
#await self.custom_delay(seconds=1) # In the simulation, we do not need to wait
|
||||
# await self.custom_delay(seconds=1) # In the simulation, we do not need to wait
|
||||
await self.aspirate(
|
||||
resources=[targets],
|
||||
vols=[0],
|
||||
use_channels=None,
|
||||
flow_rates=None,
|
||||
offsets=[Coordinate(x=targets.get_size_x()/2,y=0,z=0)],
|
||||
offsets=[Coordinate(x=targets.get_size_x() / 2, y=0, z=0)],
|
||||
liquid_height=None,
|
||||
blow_out_air_volume=None
|
||||
blow_out_air_volume=None,
|
||||
)
|
||||
|
||||
async def mix(
|
||||
@@ -298,9 +306,9 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
height_to_bottom: Optional[float] = None,
|
||||
offsets: Optional[Coordinate] = None,
|
||||
mix_rate: Optional[float] = None,
|
||||
none_keys: List[str] = []
|
||||
none_keys: List[str] = [],
|
||||
):
|
||||
if mix_time is None: # No mixing required
|
||||
if mix_time is None: # No mixing required
|
||||
return
|
||||
"""Mix the liquid in the target wells."""
|
||||
for _ in range(mix_time):
|
||||
@@ -333,7 +341,7 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
tip_iter = self.iter_tips(tip_racks)
|
||||
self.current_tip = tip_iter
|
||||
|
||||
async def move_to(self, well: Well, dis_to_top: float = 0 , channel: int = 0):
|
||||
async def move_to(self, well: Well, dis_to_top: float = 0, channel: int = 0):
|
||||
"""
|
||||
Move a single channel to a specific well with a given z-height.
|
||||
|
||||
@@ -352,4 +360,3 @@ class LiquidHandlerAbstract(LiquidHandler):
|
||||
await self.move_channel_x(channel, abs_loc.x)
|
||||
await self.move_channel_y(channel, abs_loc.y)
|
||||
await self.move_channel_z(channel, abs_loc.z + well_height + dis_to_top)
|
||||
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
|
||||
import json
|
||||
from typing import Sequence, Optional, List, Union, Literal
|
||||
json_path = "/Users/guangxinzhang/Documents/Deep Potential/opentrons/convert/protocols/enriched_steps/sci-lucif-assay4.json"
|
||||
|
||||
with open(json_path, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
transfer_example = data[0]
|
||||
#print(transfer_example)
|
||||
|
||||
|
||||
temp_protocol = []
|
||||
TipLocation = "BC1025F" # Assuming this is a fixed tip location for the transfer
|
||||
sources = transfer_example["sources"] # Assuming sources is a list of Container objects
|
||||
targets = transfer_example["targets"] # Assuming targets is a list of Container objects
|
||||
tip_racks = transfer_example["tip_racks"] # Assuming tip_racks is a list of TipRack objects
|
||||
asp_vols = transfer_example["asp_vols"] # Assuming asp_vols is a list of volumes
|
||||
solvent = "PBS"
|
||||
|
||||
def transfer_liquid(
|
||||
#self,
|
||||
sources,#: Sequence[Container],
|
||||
targets,#: Sequence[Container],
|
||||
tip_racks,#: Sequence[TipRack],
|
||||
TipLocation,
|
||||
# *,
|
||||
# use_channels: Optional[List[int]] = None,
|
||||
asp_vols: Union[List[float], float],
|
||||
solvent: Optional[str] = None,
|
||||
# dis_vols: Union[List[float], float],
|
||||
# asp_flow_rates: Optional[List[Optional[float]]] = None,
|
||||
# dis_flow_rates: Optional[List[Optional[float]]] = None,
|
||||
# offsets,#: Optional[List[]] = None,
|
||||
# touch_tip: bool = False,
|
||||
# liquid_height: Optional[List[Optional[float]]] = None,
|
||||
# blow_out_air_volume: Optional[List[Optional[float]]] = None,
|
||||
# spread: Literal["wide", "tight", "custom"] = "wide",
|
||||
# is_96_well: bool = False,
|
||||
# mix_stage: Optional[Literal["none", "before", "after", "both"]] = "none",
|
||||
# mix_times,#: Optional[list() = None,
|
||||
# mix_vol: Optional[int] = None,
|
||||
# mix_rate: Optional[int] = None,
|
||||
# mix_liquid_height: Optional[float] = None,
|
||||
# delays: Optional[List[int]] = None,
|
||||
# none_keys: List[str] = []
|
||||
):
|
||||
# -------- Build Biomek transfer step --------
|
||||
# 1) Construct default parameter scaffold (values mirror Biomek “Transfer” block).
|
||||
|
||||
transfer_params = {
|
||||
"Span8": False,
|
||||
"Pod": "Pod1",
|
||||
"items": {}, # to be filled below
|
||||
"Wash": False,
|
||||
"Dynamic?": True,
|
||||
"AutoSelectActiveWashTechnique": False,
|
||||
"ActiveWashTechnique": "",
|
||||
"ChangeTipsBetweenDests": False,
|
||||
"ChangeTipsBetweenSources": False,
|
||||
"DefaultCaption": "", # filled after we know first pair/vol
|
||||
"UseExpression": False,
|
||||
"LeaveTipsOn": False,
|
||||
"MandrelExpression": "",
|
||||
"Repeats": "1",
|
||||
"RepeatsByVolume": False,
|
||||
"Replicates": "1",
|
||||
"ShowTipHandlingDetails": False,
|
||||
"ShowTransferDetails": True,
|
||||
"Solvent": "Water",
|
||||
"Span8Wash": False,
|
||||
"Span8WashVolume": "2",
|
||||
"Span8WasteVolume": "1",
|
||||
"SplitVolume": False,
|
||||
"SplitVolumeCleaning": False,
|
||||
"Stop": "Destinations",
|
||||
"TipLocation": "BC1025F",
|
||||
"UseCurrentTips": False,
|
||||
"UseDisposableTips": True,
|
||||
"UseFixedTips": False,
|
||||
"UseJIT": True,
|
||||
"UseMandrelSelection": True,
|
||||
"UseProbes": [True, True, True, True, True, True, True, True],
|
||||
"WashCycles": "1",
|
||||
"WashVolume": "110%",
|
||||
"Wizard": False
|
||||
}
|
||||
|
||||
items: dict = {}
|
||||
for idx, (src, dst) in enumerate(zip(sources, targets)):
|
||||
items[str(idx)] = {
|
||||
"Source": str(src),
|
||||
"Destination": str(dst),
|
||||
"Volume": asp_vols[idx]
|
||||
}
|
||||
transfer_params["items"] = items
|
||||
transfer_params["Solvent"] = solvent if solvent else "Water"
|
||||
transfer_params["TipLocation"] = TipLocation
|
||||
|
||||
if len(tip_racks) == 1:
|
||||
transfer_params['UseCurrentTips'] = True
|
||||
elif len(tip_racks) > 1:
|
||||
transfer_params["ChangeTipsBetweenDests"] = True
|
||||
|
||||
return transfer_params
|
||||
|
||||
action = transfer_liquid(sources=sources,targets=targets,tip_racks=tip_racks, asp_vols=asp_vols,solvent = solvent, TipLocation=TipLocation)
|
||||
print(json.dumps(action,indent=2))
|
||||
# print(action)
|
||||
|
||||
|
||||
|
||||
|
||||
"""
|
||||
"transfer": {
|
||||
|
||||
"items": {},
|
||||
"Wash": false,
|
||||
"Dynamic?": true,
|
||||
"AutoSelectActiveWashTechnique": false,
|
||||
"ActiveWashTechnique": "",
|
||||
"ChangeTipsBetweenDests": true,
|
||||
"ChangeTipsBetweenSources": false,
|
||||
"DefaultCaption": "Transfer 100 µL from P13 to P3",
|
||||
"UseExpression": false,
|
||||
"LeaveTipsOn": false,
|
||||
"MandrelExpression": "",
|
||||
"Repeats": "1",
|
||||
"RepeatsByVolume": false,
|
||||
"Replicates": "1",
|
||||
"ShowTipHandlingDetails": true,
|
||||
"ShowTransferDetails": true,
|
||||
|
||||
"Span8Wash": false,
|
||||
"Span8WashVolume": "2",
|
||||
"Span8WasteVolume": "1",
|
||||
"SplitVolume": false,
|
||||
"SplitVolumeCleaning": false,
|
||||
"Stop": "Destinations",
|
||||
"TipLocation": "BC1025F",
|
||||
"UseCurrentTips": false,
|
||||
"UseDisposableTips": false,
|
||||
"UseFixedTips": false,
|
||||
"UseJIT": true,
|
||||
"UseMandrelSelection": true,
|
||||
"UseProbes": [true, true, true, true, true, true, true, true],
|
||||
"WashCycles": "3",
|
||||
"WashVolume": "110%",
|
||||
"Wizard": false
|
||||
"""
|
||||
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
0
unilabos/devices/mock/__init__.py
Normal file
0
unilabos/devices/mock/__init__.py
Normal file
177
unilabos/devices/mock/mock_chiller.py
Normal file
177
unilabos/devices/mock/mock_chiller.py
Normal 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("测试完成")
|
||||
235
unilabos/devices/mock/mock_filter.py
Normal file
235
unilabos/devices/mock/mock_filter.py
Normal 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("测试完成")
|
||||
247
unilabos/devices/mock/mock_heater.py
Normal file
247
unilabos/devices/mock/mock_heater.py
Normal 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测试完成")
|
||||
360
unilabos/devices/mock/mock_pump.py
Normal file
360
unilabos/devices/mock/mock_pump.py
Normal 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("测试完成")
|
||||
390
unilabos/devices/mock/mock_rotavap.py
Normal file
390
unilabos/devices/mock/mock_rotavap.py
Normal 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("测试完成")
|
||||
399
unilabos/devices/mock/mock_separator.py
Normal file
399
unilabos/devices/mock/mock_separator.py
Normal 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())
|
||||
89
unilabos/devices/mock/mock_solenoid_valve.py
Normal file
89
unilabos/devices/mock/mock_solenoid_valve.py
Normal 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("测试完成")
|
||||
307
unilabos/devices/mock/mock_stirrer.py
Normal file
307
unilabos/devices/mock/mock_stirrer.py
Normal 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("测试完成")
|
||||
229
unilabos/devices/mock/mock_stirrer_new.py
Normal file
229
unilabos/devices/mock/mock_stirrer_new.py
Normal 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
|
||||
}
|
||||
410
unilabos/devices/mock/mock_vacuum.py
Normal file
410
unilabos/devices/mock/mock_vacuum.py
Normal 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("测试完成")
|
||||
@@ -5,22 +5,22 @@ class SolenoidValveMock:
|
||||
def __init__(self, port: str = "COM6"):
|
||||
self._status = "Idle"
|
||||
self._valve_position = "OPEN"
|
||||
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
|
||||
@property
|
||||
def valve_position(self) -> str:
|
||||
return self._valve_position
|
||||
|
||||
def get_valve_position(self) -> str:
|
||||
return self._valve_position
|
||||
|
||||
|
||||
def set_valve_position(self, position):
|
||||
self._status = "Busy"
|
||||
time.sleep(5)
|
||||
|
||||
|
||||
self._valve_position = position
|
||||
time.sleep(5)
|
||||
self._status = "Idle"
|
||||
|
||||
@@ -4,18 +4,16 @@ import time
|
||||
class VacuumPumpMock:
|
||||
def __init__(self, port: str = "COM6"):
|
||||
self._status = "OPEN"
|
||||
|
||||
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self._status
|
||||
|
||||
def get_status(self) -> str:
|
||||
return self._status
|
||||
|
||||
def set_status(self, position):
|
||||
time.sleep(5)
|
||||
|
||||
self._status = position
|
||||
|
||||
def set_status(self, string):
|
||||
self._status = string
|
||||
time.sleep(5)
|
||||
|
||||
def open(self):
|
||||
|
||||
0
unilabos/devices/resource_container/__init__.py
Normal file
0
unilabos/devices/resource_container/__init__.py
Normal file
9
unilabos/devices/resource_container/container.py
Normal file
9
unilabos/devices/resource_container/container.py
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
class HotelContainer:
|
||||
def __init__(self, rotation: dict, device_config: dict):
|
||||
self.rotation = rotation
|
||||
self.device_config = device_config
|
||||
self.status = 'idle'
|
||||
|
||||
def get_rotation(self):
|
||||
return self.rotation
|
||||
38
unilabos/devices/ros_dev/lh_joint_config.json
Normal file
38
unilabos/devices/ros_dev/lh_joint_config.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"OTDeck":{
|
||||
"joint_names":[
|
||||
"first_joint",
|
||||
"second_joint",
|
||||
"third_joint",
|
||||
"fourth_joint"
|
||||
],
|
||||
"link_names":[
|
||||
"first_link",
|
||||
"second_link",
|
||||
"third_link",
|
||||
"fourth_link"
|
||||
],
|
||||
"y":{
|
||||
"first_joint":{
|
||||
"factor":-0.001,
|
||||
"offset":0.166
|
||||
}
|
||||
},
|
||||
"x":{
|
||||
"second_joint":{
|
||||
"factor":-0.001,
|
||||
"offset":0.1775
|
||||
}
|
||||
},
|
||||
"z":{
|
||||
"third_joint":{
|
||||
"factor":0.001,
|
||||
"offset":0.0
|
||||
},
|
||||
"fourth_joint":{
|
||||
"factor":0.001,
|
||||
"offset":0.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
import asyncio
|
||||
import copy
|
||||
from pathlib import Path
|
||||
import threading
|
||||
import rclpy
|
||||
import json
|
||||
import time
|
||||
from rclpy.executors import MultiThreadedExecutor
|
||||
from rclpy.action import ActionServer
|
||||
from rclpy.action import ActionServer,ActionClient
|
||||
from sensor_msgs.msg import JointState
|
||||
from unilabos_msgs.action import SendCmd
|
||||
from rclpy.action.server import ServerGoalHandle
|
||||
@@ -11,9 +14,11 @@ from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||
from tf_transformations import quaternion_from_euler
|
||||
from tf2_ros import TransformBroadcaster, Buffer, TransformListener
|
||||
|
||||
from rclpy.node import Node
|
||||
import re
|
||||
|
||||
class LiquidHandlerJointPublisher(BaseROS2DeviceNode):
|
||||
def __init__(self,device_id:str, joint_config:dict, lh_id:str,resource_tracker, rate=50):
|
||||
def __init__(self,resources_config:list, resource_tracker, rate=50, device_id:str = "lh_joint_publisher"):
|
||||
super().__init__(
|
||||
driver_instance=self,
|
||||
device_id=device_id,
|
||||
@@ -23,60 +28,118 @@ class LiquidHandlerJointPublisher(BaseROS2DeviceNode):
|
||||
print_publish=False,
|
||||
resource_tracker=resource_tracker,
|
||||
)
|
||||
|
||||
# joint_config_dict = {
|
||||
# "joint_names":[
|
||||
# "first_joint",
|
||||
# "second_joint",
|
||||
# "third_joint",
|
||||
# "fourth_joint"
|
||||
# ],
|
||||
# "y":{
|
||||
# "first_joint":{
|
||||
# "factor":-1,
|
||||
# "offset":0.0
|
||||
# }
|
||||
# },
|
||||
# "x":{
|
||||
# "second_joint":{
|
||||
# "factor":-1,
|
||||
# "offset":0.0
|
||||
# }
|
||||
# },
|
||||
# "z":{
|
||||
# "third_joint":{
|
||||
# "factor":1,
|
||||
# "offset":0.0
|
||||
# },
|
||||
# "fourth_joint":{
|
||||
# "factor":1,
|
||||
# "offset":0.0
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
|
||||
# 初始化参数
|
||||
self.j_msg = JointState()
|
||||
self.lh_id = lh_id
|
||||
# self.j_msg.name = joint_names
|
||||
self.joint_config = joint_config
|
||||
self.j_msg.position = [0.0 for i in range(len(joint_config['joint_names']))]
|
||||
self.j_msg.name = [f"{self.lh_id}_{x}" for x in joint_config['joint_names']]
|
||||
# self.joint_config = joint_config_dict
|
||||
# self.j_msg.position = [0.0 for i in range(len(joint_config_dict['joint_names']))]
|
||||
# self.j_msg.name = [f"{self.lh_id}_{x}" for x in joint_config_dict['joint_names']]
|
||||
joint_config = json.load(open(f"{Path(__file__).parent.absolute()}/lh_joint_config.json", encoding="utf-8"))
|
||||
self.resources_config = {x['id']:x for x in resources_config}
|
||||
self.rate = rate
|
||||
self.tf_buffer = Buffer()
|
||||
self.tf_listener = TransformListener(self.tf_buffer, self)
|
||||
self.j_pub = self.create_publisher(JointState,'/joint_states',10)
|
||||
self.create_timer(0.02,self.lh_joint_pub_callback)
|
||||
self.create_timer(1,self.lh_joint_pub_callback)
|
||||
|
||||
|
||||
self.resource_action = None
|
||||
|
||||
while self.resource_action is None:
|
||||
self.resource_action = self.check_tf_update_actions()
|
||||
time.sleep(1)
|
||||
|
||||
self.resource_action_client = ActionClient(self, SendCmd, self.resource_action)
|
||||
while not self.resource_action_client.wait_for_server(timeout_sec=1.0):
|
||||
self.get_logger().info('等待 TfUpdate 服务器...')
|
||||
|
||||
self.deck_list = []
|
||||
self.lh_devices = {}
|
||||
# 初始化设备ID与config信息
|
||||
for resource in resources_config:
|
||||
if resource['class'] == 'liquid_handler':
|
||||
deck_id = resource['config']['data']['children'][0]['_resource_child_name']
|
||||
deck_class = resource['config']['data']['children'][0]['_resource_type'].split(':')[-1]
|
||||
key = f'{deck_id}'
|
||||
# key = f'{resource["id"]}_{deck_id}'
|
||||
self.lh_devices[key] = {
|
||||
'joint_msg':JointState(
|
||||
name=[f'{key}_{x}' for x in joint_config[deck_class]['joint_names']],
|
||||
position=[0.0 for _ in joint_config[deck_class]['joint_names']]
|
||||
),
|
||||
'joint_config':joint_config[deck_class]
|
||||
}
|
||||
self.deck_list.append(deck_id)
|
||||
|
||||
print('='*20)
|
||||
print(self.lh_devices)
|
||||
print('='*20)
|
||||
self.j_action = ActionServer(
|
||||
self,
|
||||
SendCmd,
|
||||
"joint",
|
||||
"hl_joint_action",
|
||||
self.lh_joint_action_callback,
|
||||
result_timeout=5000
|
||||
)
|
||||
|
||||
|
||||
def check_tf_update_actions(self):
|
||||
topics = self.get_topic_names_and_types()
|
||||
|
||||
|
||||
for topic_item in topics:
|
||||
|
||||
topic_name, topic_types = topic_item
|
||||
|
||||
if 'action_msgs/msg/GoalStatusArray' in topic_types:
|
||||
# 删除 /_action/status 部分
|
||||
|
||||
base_name = topic_name.replace('/_action/status', '')
|
||||
# 检查最后一个部分是否为 tf_update
|
||||
parts = base_name.split('/')
|
||||
if parts and parts[-1] == 'tf_update':
|
||||
return base_name
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def find_resource_parent(self, resource_id:str):
|
||||
# 遍历父辈,找到父辈的父辈,直到找到设备ID
|
||||
parent_id = self.resources_config[resource_id]['parent']
|
||||
try:
|
||||
if parent_id in self.deck_list:
|
||||
p_ = self.resources_config[parent_id]['parent']
|
||||
str_ = f'{parent_id}'
|
||||
return str(str_)
|
||||
else:
|
||||
return self.find_resource_parent(parent_id)
|
||||
except Exception as e:
|
||||
return None
|
||||
|
||||
|
||||
def send_resource_action(self, resource_id_list:list[str], link_name:str):
|
||||
goal_msg = SendCmd.Goal()
|
||||
str_dict = {}
|
||||
for resource in resource_id_list:
|
||||
str_dict[resource] = link_name
|
||||
|
||||
goal_msg.command = json.dumps(str_dict)
|
||||
self.resource_action_client.send_goal(goal_msg)
|
||||
|
||||
def resource_move(self, resource_id:str, link_name:str, channels:list[int]):
|
||||
resource = resource_id.rsplit("_",1)
|
||||
|
||||
channel_list = ['A','B','C','D','E','F','G','H']
|
||||
|
||||
resource_list = []
|
||||
match = re.match(r'([a-zA-Z_]+)(\d+)', resource[1])
|
||||
if match:
|
||||
number = match.group(2)
|
||||
for channel in channels:
|
||||
resource_list.append(f"{resource[0]}_{channel_list[channel]}{number}")
|
||||
|
||||
if len(resource_list) > 0:
|
||||
self.send_resource_action(resource_list, link_name)
|
||||
|
||||
|
||||
|
||||
def lh_joint_action_callback(self,goal_handle: ServerGoalHandle):
|
||||
"""Move a single joint
|
||||
|
||||
@@ -101,12 +164,13 @@ class LiquidHandlerJointPublisher(BaseROS2DeviceNode):
|
||||
goal_handle.succeed()
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print(f'Liquid handler action error: \n{e}')
|
||||
goal_handle.abort()
|
||||
result.success = False
|
||||
|
||||
|
||||
return result
|
||||
def inverse_kinematics(self, x, y, z,
|
||||
parent_id,
|
||||
x_joint:dict,
|
||||
y_joint:dict,
|
||||
z_joint:dict ):
|
||||
@@ -117,77 +181,102 @@ class LiquidHandlerJointPublisher(BaseROS2DeviceNode):
|
||||
x (float): x坐标
|
||||
y (float): y坐标
|
||||
z (float): z坐标
|
||||
x_joint (dict): x轴关节配置,包含plus和offset
|
||||
y_joint (dict): y轴关节配置,包含plus和offset
|
||||
z_joint (dict): z轴关节配置,包含plus和offset
|
||||
x_joint (dict): x轴关节配置,包含factor和offset
|
||||
y_joint (dict): y轴关节配置,包含factor和offset
|
||||
z_joint (dict): z轴关节配置,包含factor和offset
|
||||
|
||||
Returns:
|
||||
dict: 关节名称和对应位置的字典
|
||||
"""
|
||||
joint_positions = copy.deepcopy(self.j_msg.position)
|
||||
joint_positions = copy.deepcopy(self.lh_devices[parent_id]['joint_msg'].position)
|
||||
|
||||
z_index = 0
|
||||
# 处理x轴关节
|
||||
for joint_name, config in x_joint.items():
|
||||
index = self.j_msg.name.index(f"{self.lh_id}_{joint_name}")
|
||||
index = self.lh_devices[parent_id]['joint_msg'].name.index(f"{parent_id}_{joint_name}")
|
||||
joint_positions[index] = x * config["factor"] + config["offset"]
|
||||
|
||||
# 处理y轴关节
|
||||
for joint_name, config in y_joint.items():
|
||||
index = self.j_msg.name.index(f"{self.lh_id}_{joint_name}")
|
||||
index = self.lh_devices[parent_id]['joint_msg'].name.index(f"{parent_id}_{joint_name}")
|
||||
joint_positions[index] = y * config["factor"] + config["offset"]
|
||||
|
||||
# 处理z轴关节
|
||||
for joint_name, config in z_joint.items():
|
||||
index = self.j_msg.name.index(f"{self.lh_id}_{joint_name}")
|
||||
index = self.lh_devices[parent_id]['joint_msg'].name.index(f"{parent_id}_{joint_name}")
|
||||
joint_positions[index] = z * config["factor"] + config["offset"]
|
||||
|
||||
|
||||
return joint_positions
|
||||
z_index = index
|
||||
|
||||
return joint_positions ,z_index
|
||||
|
||||
|
||||
def move_joints(self, resource_name, link_name, speed, x_joint=None, y_joint=None, z_joint=None):
|
||||
def move_joints(self, resource_names, x, y, z, option, speed = 0.1 ,x_joint=None, y_joint=None, z_joint=None):
|
||||
if isinstance(resource_names, list):
|
||||
resource_name_ = resource_names[0]
|
||||
else:
|
||||
resource_name_ = resource_names
|
||||
|
||||
transform = self.tf_buffer.lookup_transform(
|
||||
link_name,
|
||||
resource_name,
|
||||
rclpy.time.Time()
|
||||
)
|
||||
x,y,z = transform.transform.translation.x, transform.transform.translation.y, transform.transform.translation.z
|
||||
parent_id = self.find_resource_parent(resource_name_)
|
||||
|
||||
|
||||
print('!'*20)
|
||||
print(parent_id)
|
||||
print('!'*20)
|
||||
if x_joint is None:
|
||||
x_joint_config = next(iter(self.joint_config['x'].items()))
|
||||
elif x_joint in self.joint_config['x']:
|
||||
x_joint_config = self.joint_config['x'][x_joint]
|
||||
xa,xb = next(iter(self.lh_devices[parent_id]['joint_config']['x'].items()))
|
||||
x_joint_config = {xa:xb}
|
||||
elif x_joint in self.lh_devices[parent_id]['joint_config']['x']:
|
||||
x_joint_config = self.lh_devices[parent_id]['joint_config']['x'][x_joint]
|
||||
else:
|
||||
raise ValueError(f"x_joint {x_joint} not in joint_config['x']")
|
||||
if y_joint is None:
|
||||
y_joint_config = next(iter(self.joint_config['y'].items()))
|
||||
elif y_joint in self.joint_config['y']:
|
||||
y_joint_config = self.joint_config['y'][y_joint]
|
||||
ya,yb = next(iter(self.lh_devices[parent_id]['joint_config']['y'].items()))
|
||||
y_joint_config = {ya:yb}
|
||||
elif y_joint in self.lh_devices[parent_id]['joint_config']['y']:
|
||||
y_joint_config = self.lh_devices[parent_id]['joint_config']['y'][y_joint]
|
||||
else:
|
||||
raise ValueError(f"y_joint {y_joint} not in joint_config['y']")
|
||||
if z_joint is None:
|
||||
z_joint_config = next(iter(self.joint_config['z'].items()))
|
||||
elif z_joint in self.joint_config['z']:
|
||||
z_joint_config = self.joint_config['z'][z_joint]
|
||||
za, zb = next(iter(self.lh_devices[parent_id]['joint_config']['z'].items()))
|
||||
z_joint_config = {za :zb}
|
||||
elif z_joint in self.lh_devices[parent_id]['joint_config']['z']:
|
||||
z_joint_config = self.lh_devices[parent_id]['joint_config']['z'][z_joint]
|
||||
else:
|
||||
raise ValueError(f"z_joint {z_joint} not in joint_config['z']")
|
||||
joint_positions_target = self.inverse_kinematics(x,y,z,x_joint_config,y_joint_config,z_joint_config)
|
||||
|
||||
joint_positions_target, z_index = self.inverse_kinematics(x,y,z,parent_id,x_joint_config,y_joint_config,z_joint_config)
|
||||
joint_positions_target_zero = copy.deepcopy(joint_positions_target)
|
||||
joint_positions_target_zero[z_index] = 0
|
||||
|
||||
self.move_to(joint_positions_target_zero, speed, parent_id)
|
||||
self.move_to(joint_positions_target, speed, parent_id)
|
||||
time.sleep(1)
|
||||
if option == "pick":
|
||||
link_name = self.lh_devices[parent_id]['joint_config']['link_names'][z_index]
|
||||
link_name = f'{parent_id}_{link_name}'
|
||||
self.resource_move(resource_name_, link_name, [0,1,2,3,4,5,6,7])
|
||||
elif option == "drop_trash":
|
||||
self.resource_move(resource_name_, "__trash", [0,1,2,3,4,5,6,7])
|
||||
elif option == "drop":
|
||||
self.resource_move(resource_name_, "world", [0,1,2,3,4,5,6,7])
|
||||
self.move_to(joint_positions_target_zero, speed, parent_id)
|
||||
|
||||
|
||||
def move_to(self, joint_positions ,speed, parent_id):
|
||||
loop_flag = 0
|
||||
|
||||
|
||||
while loop_flag < len(self.joint_config['joint_names']):
|
||||
while loop_flag < len(joint_positions):
|
||||
loop_flag = 0
|
||||
for i in range(len(self.joint_config['joint_names'])):
|
||||
distance = joint_positions_target[i] - self.j_msg.position[i]
|
||||
for i in range(len(joint_positions)):
|
||||
distance = joint_positions[i] - self.lh_devices[parent_id]['joint_msg'].position[i]
|
||||
if distance == 0:
|
||||
loop_flag += 1
|
||||
continue
|
||||
minus_flag = distance/abs(distance)
|
||||
if abs(distance) > speed/self.rate:
|
||||
self.j_msg.position[i] += minus_flag * speed/self.rate
|
||||
self.lh_devices[parent_id]['joint_msg'].position[i] += minus_flag * speed/self.rate
|
||||
else :
|
||||
self.j_msg.position[i] = joint_positions_target[i]
|
||||
self.lh_devices[parent_id]['joint_msg'].position[i] = joint_positions[i]
|
||||
loop_flag += 1
|
||||
|
||||
|
||||
@@ -195,10 +284,103 @@ class LiquidHandlerJointPublisher(BaseROS2DeviceNode):
|
||||
self.lh_joint_pub_callback()
|
||||
time.sleep(1/self.rate)
|
||||
|
||||
|
||||
def lh_joint_pub_callback(self):
|
||||
self.j_msg.header.stamp = self.get_clock().now().to_msg()
|
||||
self.j_pub.publish(self.j_msg)
|
||||
for id, config in self.lh_devices.items():
|
||||
config['joint_msg'].header.stamp = self.get_clock().now().to_msg()
|
||||
self.j_pub.publish(config['joint_msg'])
|
||||
|
||||
|
||||
|
||||
|
||||
class JointStatePublisher(Node):
|
||||
def __init__(self):
|
||||
super().__init__('joint_state_publisher')
|
||||
|
||||
self.lh_action = None
|
||||
|
||||
while self.lh_action is None:
|
||||
self.lh_action = self.check_hl_joint_actions()
|
||||
time.sleep(1)
|
||||
|
||||
self.lh_action_client = ActionClient(self, SendCmd, self.lh_action)
|
||||
while not self.lh_action_client.wait_for_server(timeout_sec=1.0):
|
||||
self.get_logger().info('等待 TfUpdate 服务器...')
|
||||
|
||||
|
||||
|
||||
def check_hl_joint_actions(self):
|
||||
topics = self.get_topic_names_and_types()
|
||||
|
||||
|
||||
for topic_item in topics:
|
||||
|
||||
topic_name, topic_types = topic_item
|
||||
|
||||
if 'action_msgs/msg/GoalStatusArray' in topic_types:
|
||||
# 删除 /_action/status 部分
|
||||
|
||||
base_name = topic_name.replace('/_action/status', '')
|
||||
# 检查最后一个部分是否为 tf_update
|
||||
parts = base_name.split('/')
|
||||
if parts and parts[-1] == 'hl_joint_action':
|
||||
return base_name
|
||||
|
||||
return None
|
||||
|
||||
def send_resource_action(self, resource_name, x,y,z,option, speed = 0.1,x_joint=None, y_joint=None, z_joint=None):
|
||||
goal_msg = SendCmd.Goal()
|
||||
str_dict = {
|
||||
'resource_names':resource_name,
|
||||
'x':x,
|
||||
'y':y,
|
||||
'z':z,
|
||||
'option':option,
|
||||
'speed':speed,
|
||||
'x_joint':x_joint,
|
||||
'y_joint':y_joint,
|
||||
'z_joint':z_joint
|
||||
}
|
||||
|
||||
|
||||
goal_msg.command = json.dumps(str_dict)
|
||||
|
||||
if not self.lh_action_client.wait_for_server(timeout_sec=5.0):
|
||||
self.get_logger().error('Action server not available')
|
||||
return None
|
||||
|
||||
try:
|
||||
# 创建新的executor
|
||||
executor = rclpy.executors.MultiThreadedExecutor()
|
||||
executor.add_node(self)
|
||||
|
||||
# 发送目标
|
||||
future = self.lh_action_client.send_goal_async(goal_msg)
|
||||
|
||||
# 使用executor等待结果
|
||||
while not future.done():
|
||||
executor.spin_once(timeout_sec=0.1)
|
||||
|
||||
handle = future.result()
|
||||
|
||||
if not handle.accepted:
|
||||
self.get_logger().error('Goal was rejected')
|
||||
return None
|
||||
|
||||
# 等待最终结果
|
||||
result_future = handle.get_result_async()
|
||||
while not result_future.done():
|
||||
executor.spin_once(timeout_sec=0.1)
|
||||
|
||||
result = result_future.result()
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
self.get_logger().error(f'Error during action execution: {str(e)}')
|
||||
return None
|
||||
finally:
|
||||
# 清理executor
|
||||
executor.remove_node(self)
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
|
||||
2442
unilabos/devices/ros_dev/moveit2.py
Normal file
2442
unilabos/devices/ros_dev/moveit2.py
Normal file
File diff suppressed because it is too large
Load Diff
384
unilabos/devices/ros_dev/moveit_interface.py
Normal file
384
unilabos/devices/ros_dev/moveit_interface.py
Normal file
@@ -0,0 +1,384 @@
|
||||
import json
|
||||
import time
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
|
||||
from moveit_msgs.msg import JointConstraint, Constraints
|
||||
from rclpy.action import ActionClient
|
||||
from tf2_ros import Buffer, TransformListener
|
||||
from unilabos_msgs.action import SendCmd
|
||||
|
||||
from unilabos.devices.ros_dev.moveit2 import MoveIt2
|
||||
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
|
||||
|
||||
|
||||
class MoveitInterface:
|
||||
_ros_node: BaseROS2DeviceNode
|
||||
tf_buffer: Buffer
|
||||
tf_listener: TransformListener
|
||||
|
||||
def __init__(self, moveit_type, joint_poses, rotation=None, device_config=None):
|
||||
self.device_config = device_config
|
||||
self.rotation = rotation
|
||||
self.data_config = json.load(
|
||||
open(
|
||||
f"{Path(__file__).parent.parent.parent.absolute()}/device_mesh/devices/{moveit_type}/config/move_group.json",
|
||||
encoding="utf-8",
|
||||
)
|
||||
)
|
||||
self.arm_move_flag = False
|
||||
self.move_option = ["pick", "place", "side_pick", "side_place"]
|
||||
self.joint_poses = joint_poses
|
||||
self.cartesian_flag = False
|
||||
self.mesh_group = ["reactor", "sample", "beaker"]
|
||||
self.moveit2 = {}
|
||||
self.resource_action = None
|
||||
self.resource_client = None
|
||||
self.resource_action_ok = False
|
||||
|
||||
|
||||
def post_init(self, ros_node: BaseROS2DeviceNode):
|
||||
self._ros_node = ros_node
|
||||
self.tf_buffer = Buffer()
|
||||
self.tf_listener = TransformListener(self.tf_buffer, self._ros_node)
|
||||
|
||||
for move_group, config in self.data_config.items():
|
||||
|
||||
base_link_name = f"{self._ros_node.device_id}_{config['base_link_name']}"
|
||||
end_effector_name = f"{self._ros_node.device_id}_{config['end_effector_name']}"
|
||||
joint_names = [f"{self._ros_node.device_id}_{name}" for name in config["joint_names"]]
|
||||
|
||||
self.moveit2[f"{move_group}"] = MoveIt2(
|
||||
node=self._ros_node,
|
||||
joint_names=joint_names,
|
||||
base_link_name=base_link_name,
|
||||
end_effector_name=end_effector_name,
|
||||
group_name=f"{self._ros_node.device_id}_{move_group}",
|
||||
callback_group=self._ros_node.callback_group,
|
||||
use_move_group_action=True,
|
||||
ignore_new_calls_while_executing=True,
|
||||
)
|
||||
self.moveit2[f"{move_group}"].allowed_planning_time = 3.0
|
||||
|
||||
self._ros_node.create_timer(1, self.wait_for_resource_action, callback_group=self._ros_node.callback_group)
|
||||
|
||||
|
||||
def wait_for_resource_action(self):
|
||||
if not self.resource_action_ok:
|
||||
|
||||
while self.resource_action is None:
|
||||
self.resource_action = self.check_tf_update_actions()
|
||||
time.sleep(1)
|
||||
self.resource_client = ActionClient(self._ros_node, SendCmd, self.resource_action)
|
||||
self.resource_action_ok = True
|
||||
while not self.resource_client.wait_for_server(timeout_sec=5.0):
|
||||
self._ros_node.lab_logger().info("等待 TfUpdate 服务器...")
|
||||
|
||||
def check_tf_update_actions(self):
|
||||
topics = self._ros_node.get_topic_names_and_types()
|
||||
for topic_item in topics:
|
||||
topic_name, topic_types = topic_item
|
||||
if "action_msgs/msg/GoalStatusArray" in topic_types:
|
||||
# 删除 /_action/status 部分
|
||||
|
||||
base_name = topic_name.replace("/_action/status", "")
|
||||
# 检查最后一个部分是否为 tf_update
|
||||
parts = base_name.split("/")
|
||||
if parts and parts[-1] == "tf_update":
|
||||
return base_name
|
||||
|
||||
return None
|
||||
|
||||
def set_position(self, command):
|
||||
"""使用moveit 移动到指定点
|
||||
Args:
|
||||
command: A JSON-formatted string that includes quaternion, speed, position
|
||||
|
||||
position (list) : 点的位置 [x,y,z]
|
||||
quaternion (list) : 点的姿态(四元数) [x,y,z,w]
|
||||
move_group (string) : The move group moveit will plan
|
||||
speed (float) : The speed of the movement, speed > 0
|
||||
retry (int) : Retry times when moveit plan fails
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
result = SendCmd.Result()
|
||||
cmd_str = command.replace("'", '"')
|
||||
cmd_dict = json.loads(cmd_str)
|
||||
self.moveit_task(**cmd_dict)
|
||||
return result
|
||||
|
||||
def moveit_task(
|
||||
self, move_group, position, quaternion, speed=1, retry=10, cartesian=False, target_link=None, offsets=[0, 0, 0]
|
||||
):
|
||||
|
||||
speed_ = float(max(0.1, min(speed, 1)))
|
||||
|
||||
self.moveit2[move_group].max_velocity = speed_
|
||||
self.moveit2[move_group].max_acceleration = speed_
|
||||
|
||||
re_ = False
|
||||
|
||||
pose_result = [x + y for x, y in zip(position, offsets)]
|
||||
# print(pose_result)
|
||||
|
||||
while retry > -1 and not re_:
|
||||
|
||||
self.moveit2[move_group].move_to_pose(
|
||||
target_link=target_link,
|
||||
position=pose_result,
|
||||
quat_xyzw=quaternion,
|
||||
cartesian=cartesian,
|
||||
# cartesian_fraction_threshold=0.0,
|
||||
cartesian_max_step=0.01,
|
||||
weight_position=1.0,
|
||||
)
|
||||
re_ = self.moveit2[move_group].wait_until_executed()
|
||||
retry += -1
|
||||
|
||||
return re_
|
||||
|
||||
def moveit_joint_task(self, move_group, joint_positions, joint_names=None, speed=1, retry=10):
|
||||
|
||||
re_ = False
|
||||
|
||||
joint_positions_ = [float(x) for x in joint_positions]
|
||||
|
||||
speed_ = float(max(0.1, min(speed, 1)))
|
||||
|
||||
self.moveit2[move_group].max_velocity = speed_
|
||||
self.moveit2[move_group].max_acceleration = speed_
|
||||
|
||||
while retry > -1 and not re_:
|
||||
|
||||
self.moveit2[move_group].move_to_configuration(joint_positions=joint_positions_, joint_names=joint_names)
|
||||
re_ = self.moveit2[move_group].wait_until_executed()
|
||||
|
||||
retry += -1
|
||||
print(self.moveit2[move_group].compute_fk(joint_positions))
|
||||
return re_
|
||||
|
||||
def resource_manager(self, resource, parent_link):
|
||||
goal_msg = SendCmd.Goal()
|
||||
str_dict = {}
|
||||
str_dict[resource] = parent_link
|
||||
|
||||
goal_msg.command = json.dumps(str_dict)
|
||||
assert self.resource_client is not None
|
||||
self.resource_client.send_goal(goal_msg)
|
||||
|
||||
return True
|
||||
|
||||
def pick_and_place(self, command: str):
|
||||
"""
|
||||
Using MoveIt to make the robotic arm pick or place materials to a target point.
|
||||
|
||||
Args:
|
||||
command: A JSON-formatted string that includes option, target, speed, lift_height, mt_height
|
||||
|
||||
*option (string) : Action type: pick/place/side_pick/side_place
|
||||
*move_group (string): The move group moveit will plan
|
||||
*status(string) : Target pose
|
||||
resource(string) : The target resource
|
||||
x_distance (float) : The distance to the target in x direction(meters)
|
||||
y_distance (float) : The distance to the target in y direction(meters)
|
||||
lift_height (float) : The height at which the material should be lifted(meters)
|
||||
retry (float) : Retry times when moveit plan fails
|
||||
speed (float) : The speed of the movement, speed > 0
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
result = SendCmd.Result()
|
||||
|
||||
try:
|
||||
cmd_str = str(command).replace("'", '"')
|
||||
cmd_dict = json.loads(cmd_str)
|
||||
|
||||
if cmd_dict["option"] in self.move_option:
|
||||
option_index = self.move_option.index(cmd_dict["option"])
|
||||
place_flag = option_index % 2
|
||||
|
||||
config = {}
|
||||
function_list = []
|
||||
|
||||
status = cmd_dict["status"]
|
||||
joint_positions_ = self.joint_poses[cmd_dict["move_group"]][status]
|
||||
|
||||
config.update({k: cmd_dict[k] for k in ["speed", "retry", "move_group"] if k in cmd_dict})
|
||||
|
||||
# 夹取
|
||||
if not place_flag:
|
||||
if "target" in cmd_dict.keys():
|
||||
function_list.append(lambda: self.resource_manager(cmd_dict["resource"], cmd_dict["target"]))
|
||||
else:
|
||||
function_list.append(
|
||||
lambda: self.resource_manager(
|
||||
cmd_dict["resource"], self.moveit2[cmd_dict["move_group"]].end_effector_name
|
||||
)
|
||||
)
|
||||
else:
|
||||
function_list.append(lambda: self.resource_manager(cmd_dict["resource"], "world"))
|
||||
|
||||
constraints = []
|
||||
if "constraints" in cmd_dict.keys():
|
||||
|
||||
for i in range(len(cmd_dict["constraints"])):
|
||||
v = float(cmd_dict["constraints"][i])
|
||||
if v > 0:
|
||||
constraints.append(
|
||||
JointConstraint(
|
||||
joint_name=self.moveit2[cmd_dict["move_group"]].joint_names[i],
|
||||
position=joint_positions_[i],
|
||||
tolerance_above=v,
|
||||
tolerance_below=v,
|
||||
weight=1.0,
|
||||
)
|
||||
)
|
||||
|
||||
if "lift_height" in cmd_dict.keys():
|
||||
retval = None
|
||||
retry = config.get("retry", 10)
|
||||
while retval is None and retry > 0:
|
||||
retval = self.moveit2[cmd_dict["move_group"]].compute_fk(joint_positions_)
|
||||
time.sleep(0.1)
|
||||
retry -= 1
|
||||
if retval is None:
|
||||
result.success = False
|
||||
return result
|
||||
pose = [retval.pose.position.x, retval.pose.position.y, retval.pose.position.z]
|
||||
quaternion = [
|
||||
retval.pose.orientation.x,
|
||||
retval.pose.orientation.y,
|
||||
retval.pose.orientation.z,
|
||||
retval.pose.orientation.w,
|
||||
]
|
||||
|
||||
function_list = [
|
||||
lambda: self.moveit_task(
|
||||
position=[retval.pose.position.x, retval.pose.position.y, retval.pose.position.z],
|
||||
quaternion=quaternion,
|
||||
**config,
|
||||
cartesian=self.cartesian_flag,
|
||||
)
|
||||
] + function_list
|
||||
|
||||
pose[2] += float(cmd_dict["lift_height"])
|
||||
function_list.append(
|
||||
lambda: self.moveit_task(
|
||||
position=pose, quaternion=quaternion, **config, cartesian=self.cartesian_flag
|
||||
)
|
||||
)
|
||||
end_pose = pose
|
||||
|
||||
if "x_distance" in cmd_dict.keys() or "y_distance" in cmd_dict.keys():
|
||||
if "x_distance" in cmd_dict.keys():
|
||||
deep_pose = deepcopy(pose)
|
||||
deep_pose[0] += float(cmd_dict["x_distance"])
|
||||
elif "y_distance" in cmd_dict.keys():
|
||||
deep_pose = deepcopy(pose)
|
||||
deep_pose[1] += float(cmd_dict["y_distance"])
|
||||
|
||||
function_list = [
|
||||
lambda: self.moveit_task(
|
||||
position=pose, quaternion=quaternion, **config, cartesian=self.cartesian_flag
|
||||
)
|
||||
] + function_list
|
||||
function_list.append(
|
||||
lambda: self.moveit_task(
|
||||
position=deep_pose, quaternion=quaternion, **config, cartesian=self.cartesian_flag
|
||||
)
|
||||
)
|
||||
end_pose = deep_pose
|
||||
|
||||
retval_ik = None
|
||||
retry = config.get("retry", 10)
|
||||
while retval_ik is None and retry > 0:
|
||||
retval_ik = self.moveit2[cmd_dict["move_group"]].compute_ik(
|
||||
position=end_pose, quat_xyzw=quaternion, constraints=Constraints(joint_constraints=constraints)
|
||||
)
|
||||
time.sleep(0.1)
|
||||
retry -= 1
|
||||
if retval_ik is None:
|
||||
result.success = False
|
||||
return result
|
||||
position_ = [
|
||||
retval_ik.position[retval_ik.name.index(i)]
|
||||
for i in self.moveit2[cmd_dict["move_group"]].joint_names
|
||||
]
|
||||
function_list = [
|
||||
lambda: self.moveit_joint_task(
|
||||
joint_positions=position_,
|
||||
joint_names=self.moveit2[cmd_dict["move_group"]].joint_names,
|
||||
**config,
|
||||
)
|
||||
] + function_list
|
||||
else:
|
||||
function_list = [
|
||||
lambda: self.moveit_joint_task(**config, joint_positions=joint_positions_)
|
||||
] + function_list
|
||||
|
||||
for i in range(len(function_list)):
|
||||
if i == 0:
|
||||
self.cartesian_flag = False
|
||||
else:
|
||||
self.cartesian_flag = True
|
||||
|
||||
re = function_list[i]()
|
||||
if not re:
|
||||
print(i, re)
|
||||
result.success = False
|
||||
return result
|
||||
result.success = True
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
self.cartesian_flag = False
|
||||
result.success = False
|
||||
|
||||
return result
|
||||
|
||||
def set_status(self, command: str):
|
||||
"""
|
||||
Goto home position
|
||||
|
||||
Args:
|
||||
command: A JSON-formatted string that includes speed
|
||||
*status (string) : The joint status moveit will plan
|
||||
*move_group (string): The move group moveit will plan
|
||||
separate (list) : The joint index to be separated
|
||||
lift_height (float) : The height at which the material should be lifted(meters)
|
||||
x_distance (float) : The distance to the target in x direction(meters)
|
||||
y_distance (float) : The distance to the target in y direction(meters)
|
||||
speed (float) : The speed of the movement, speed > 0
|
||||
retry (float) : Retry times when moveit plan fails
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
result = SendCmd.Result()
|
||||
|
||||
try:
|
||||
cmd_str = command.replace("'", '"')
|
||||
cmd_dict = json.loads(cmd_str)
|
||||
config = {}
|
||||
config["move_group"] = cmd_dict["move_group"]
|
||||
if "speed" in cmd_dict.keys():
|
||||
config["speed"] = cmd_dict["speed"]
|
||||
if "retry" in cmd_dict.keys():
|
||||
config["retry"] = cmd_dict["retry"]
|
||||
|
||||
status = cmd_dict["status"]
|
||||
joint_positions_ = self.joint_poses[cmd_dict["move_group"]][status]
|
||||
re = self.moveit_joint_task(**config, joint_positions=joint_positions_)
|
||||
if not re:
|
||||
result.success = False
|
||||
return result
|
||||
result.success = True
|
||||
except Exception as e:
|
||||
print(e)
|
||||
result.success = False
|
||||
|
||||
return result
|
||||
0
unilabos/devices/virtual/__init__.py
Normal file
0
unilabos/devices/virtual/__init__.py
Normal file
158
unilabos/devices/virtual/virtual_centrifuge.py
Normal file
158
unilabos/devices/virtual/virtual_centrifuge.py
Normal file
@@ -0,0 +1,158 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
|
||||
class VirtualCentrifuge:
|
||||
"""Virtual centrifuge device for CentrifugeProtocol 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_centrifuge"
|
||||
self.config = config or {}
|
||||
|
||||
self.logger = logging.getLogger(f"VirtualCentrifuge.{self.device_id}")
|
||||
self.data = {}
|
||||
|
||||
# 添加调试信息
|
||||
print(f"=== VirtualCentrifuge {self.device_id} is being created! ===")
|
||||
print(f"=== Config: {self.config} ===")
|
||||
print(f"=== Kwargs: {kwargs} ===")
|
||||
|
||||
# 从config或kwargs中获取配置参数
|
||||
self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL')
|
||||
self._max_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"""
|
||||
print(f"=== VirtualCentrifuge {self.device_id} initialize() called! ===")
|
||||
self.logger.info(f"Initializing virtual centrifuge {self.device_id}")
|
||||
self.data.update({
|
||||
"status": "Idle",
|
||||
"current_speed": 0.0,
|
||||
"target_speed": 0.0,
|
||||
"current_temp": 25.0,
|
||||
"target_temp": 25.0,
|
||||
"max_speed": self._max_speed,
|
||||
"max_temp": self._max_temp,
|
||||
"min_temp": self._min_temp,
|
||||
"centrifuge_state": "Stopped",
|
||||
"time_remaining": 0.0,
|
||||
"progress": 0.0,
|
||||
"message": ""
|
||||
})
|
||||
return True
|
||||
|
||||
async def cleanup(self) -> bool:
|
||||
"""Cleanup virtual centrifuge"""
|
||||
self.logger.info(f"Cleaning up virtual centrifuge {self.device_id}")
|
||||
return True
|
||||
|
||||
async def centrifuge(self, vessel: str, speed: float, time: float, temp: float = 25.0) -> bool:
|
||||
"""Execute centrifuge action - matches Centrifuge action"""
|
||||
self.logger.info(f"Centrifuge: vessel={vessel}, speed={speed} RPM, time={time}s, temp={temp}°C")
|
||||
|
||||
# 验证参数
|
||||
if speed > self._max_speed:
|
||||
self.logger.error(f"Speed {speed} exceeds maximum {self._max_speed}")
|
||||
self.data["message"] = f"速度 {speed} 超过最大值 {self._max_speed}"
|
||||
return False
|
||||
|
||||
if temp > self._max_temp or temp < self._min_temp:
|
||||
self.logger.error(f"Temperature {temp} outside range {self._min_temp}-{self._max_temp}")
|
||||
self.data["message"] = f"温度 {temp} 超出范围 {self._min_temp}-{self._max_temp}"
|
||||
return False
|
||||
|
||||
# 开始离心
|
||||
self.data.update({
|
||||
"status": "Running",
|
||||
"centrifuge_state": "Centrifuging",
|
||||
"target_speed": speed,
|
||||
"current_speed": speed,
|
||||
"target_temp": temp,
|
||||
"current_temp": temp,
|
||||
"time_remaining": time,
|
||||
"vessel": vessel,
|
||||
"progress": 0.0,
|
||||
"message": f"离心中: {vessel} at {speed} RPM"
|
||||
})
|
||||
|
||||
# 模拟离心过程
|
||||
simulation_time = min(time, 5.0) # 最多等待5秒用于测试
|
||||
await asyncio.sleep(simulation_time)
|
||||
|
||||
# 离心完成
|
||||
self.data.update({
|
||||
"status": "Idle",
|
||||
"centrifuge_state": "Stopped",
|
||||
"current_speed": 0.0,
|
||||
"target_speed": 0.0,
|
||||
"time_remaining": 0.0,
|
||||
"progress": 100.0,
|
||||
"message": f"离心完成: {vessel}"
|
||||
})
|
||||
|
||||
self.logger.info(f"Centrifuge completed for vessel {vessel}")
|
||||
return True
|
||||
|
||||
# 状态属性
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self.data.get("status", "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.data.get("max_speed", self._max_speed)
|
||||
|
||||
@property
|
||||
def max_temp(self) -> float:
|
||||
return self.data.get("max_temp", self._max_temp)
|
||||
|
||||
@property
|
||||
def min_temp(self) -> float:
|
||||
return self.data.get("min_temp", self._min_temp)
|
||||
|
||||
@property
|
||||
def centrifuge_state(self) -> str:
|
||||
return self.data.get("centrifuge_state", "Unknown")
|
||||
|
||||
@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", "")
|
||||
132
unilabos/devices/virtual/virtual_column.py
Normal file
132
unilabos/devices/virtual/virtual_column.py
Normal 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")
|
||||
151
unilabos/devices/virtual/virtual_filter.py
Normal file
151
unilabos/devices/virtual/virtual_filter.py
Normal file
@@ -0,0 +1,151 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Dict, Any
|
||||
|
||||
class VirtualFilter:
|
||||
"""Virtual filter device for FilterProtocol 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_filter"
|
||||
self.config = config or {}
|
||||
|
||||
self.logger = logging.getLogger(f"VirtualFilter.{self.device_id}")
|
||||
self.data = {}
|
||||
|
||||
# 添加调试信息
|
||||
print(f"=== VirtualFilter {self.device_id} is being created! ===")
|
||||
print(f"=== Config: {self.config} ===")
|
||||
print(f"=== Kwargs: {kwargs} ===")
|
||||
|
||||
# 从config或kwargs中获取配置参数
|
||||
self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL')
|
||||
self._max_temp = self.config.get('max_temp') or kwargs.get('max_temp', 100.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', '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 filter"""
|
||||
print(f"=== VirtualFilter {self.device_id} initialize() called! ===")
|
||||
self.logger.info(f"Initializing virtual filter {self.device_id}")
|
||||
self.data.update({
|
||||
"status": "Idle",
|
||||
"filter_state": "Ready",
|
||||
"current_temp": 25.0,
|
||||
"target_temp": 25.0,
|
||||
"max_temp": self._max_temp,
|
||||
"stir_speed": 0.0,
|
||||
"max_stir_speed": self._max_stir_speed,
|
||||
"filtered_volume": 0.0,
|
||||
"progress": 0.0,
|
||||
"message": ""
|
||||
})
|
||||
return True
|
||||
|
||||
async def cleanup(self) -> bool:
|
||||
"""Cleanup virtual filter"""
|
||||
self.logger.info(f"Cleaning up virtual filter {self.device_id}")
|
||||
return True
|
||||
|
||||
async def filter_sample(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 - matches Filter action"""
|
||||
self.logger.info(f"Filter: vessel={vessel}, filtrate_vessel={filtrate_vessel}, stir={stir}, volume={volume}")
|
||||
|
||||
# 验证参数
|
||||
if temp > self._max_temp:
|
||||
self.logger.error(f"Temperature {temp} exceeds maximum {self._max_temp}")
|
||||
self.data["message"] = f"温度 {temp} 超过最大值 {self._max_temp}"
|
||||
return False
|
||||
|
||||
if stir and stir_speed > self._max_stir_speed:
|
||||
self.logger.error(f"Stir speed {stir_speed} exceeds maximum {self._max_stir_speed}")
|
||||
self.data["message"] = f"搅拌速度 {stir_speed} 超过最大值 {self._max_stir_speed}"
|
||||
return False
|
||||
|
||||
# 开始过滤
|
||||
self.data.update({
|
||||
"status": "Running",
|
||||
"filter_state": "Filtering",
|
||||
"target_temp": temp,
|
||||
"current_temp": temp,
|
||||
"stir_speed": stir_speed if stir else 0.0,
|
||||
"vessel": vessel,
|
||||
"filtrate_vessel": filtrate_vessel,
|
||||
"target_volume": volume,
|
||||
"progress": 0.0,
|
||||
"message": f"过滤中: {vessel}"
|
||||
})
|
||||
|
||||
# 模拟过滤过程
|
||||
simulation_time = min(volume / 10.0 if volume > 0 else 5.0, 10.0)
|
||||
await asyncio.sleep(simulation_time)
|
||||
|
||||
# 过滤完成
|
||||
filtered_vol = volume if volume > 0 else 50.0 # 默认过滤量
|
||||
self.data.update({
|
||||
"status": "Idle",
|
||||
"filter_state": "Ready",
|
||||
"current_temp": 25.0 if not continue_heatchill else temp,
|
||||
"target_temp": 25.0 if not continue_heatchill else temp,
|
||||
"stir_speed": 0.0 if not stir else stir_speed,
|
||||
"filtered_volume": filtered_vol,
|
||||
"progress": 100.0,
|
||||
"message": f"过滤完成: {filtered_vol}mL"
|
||||
})
|
||||
|
||||
self.logger.info(f"Filter completed: {filtered_vol}mL from {vessel}")
|
||||
return True
|
||||
|
||||
# 状态属性
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self.data.get("status", "Unknown")
|
||||
|
||||
@property
|
||||
def filter_state(self) -> str:
|
||||
return self.data.get("filter_state", "Unknown")
|
||||
|
||||
@property
|
||||
def current_temp(self) -> float:
|
||||
return self.data.get("current_temp", 25.0)
|
||||
|
||||
@property
|
||||
def target_temp(self) -> float:
|
||||
return self.data.get("target_temp", 25.0)
|
||||
|
||||
@property
|
||||
def max_temp(self) -> float:
|
||||
return self.data.get("max_temp", self._max_temp)
|
||||
|
||||
@property
|
||||
def stir_speed(self) -> float:
|
||||
return self.data.get("stir_speed", 0.0)
|
||||
|
||||
@property
|
||||
def max_stir_speed(self) -> float:
|
||||
return self.data.get("max_stir_speed", self._max_stir_speed)
|
||||
|
||||
@property
|
||||
def filtered_volume(self) -> float:
|
||||
return self.data.get("filtered_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", "")
|
||||
107
unilabos/devices/virtual/virtual_heatchill.py
Normal file
107
unilabos/devices/virtual/virtual_heatchill.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import asyncio
|
||||
import logging
|
||||
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 = {}
|
||||
|
||||
# 添加调试信息
|
||||
print(f"=== VirtualHeatChill {self.device_id} is being created! ===")
|
||||
print(f"=== Config: {self.config} ===")
|
||||
print(f"=== Kwargs: {kwargs} ===")
|
||||
|
||||
# 从config或kwargs中获取配置参数
|
||||
self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL')
|
||||
self._max_temp = self.config.get('max_temp') or kwargs.get('max_temp', 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"""
|
||||
print(f"=== VirtualHeatChill {self.device_id} initialize() called! ===")
|
||||
self.logger.info(f"Initializing virtual heat chill {self.device_id}")
|
||||
self.data.update({
|
||||
"status": "Idle"
|
||||
})
|
||||
return True
|
||||
|
||||
async def cleanup(self) -> bool:
|
||||
"""Cleanup virtual heat chill"""
|
||||
self.logger.info(f"Cleaning up virtual heat chill {self.device_id}")
|
||||
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 - matches HeatChill action exactly"""
|
||||
self.logger.info(f"HeatChill: vessel={vessel}, temp={temp}°C, time={time}s, stir={stir}, stir_speed={stir_speed}, purpose={purpose}")
|
||||
|
||||
# 验证参数
|
||||
if temp > self._max_temp or temp < self._min_temp:
|
||||
self.logger.error(f"Temperature {temp} outside range {self._min_temp}-{self._max_temp}")
|
||||
self.data["status"] = f"温度 {temp} 超出范围"
|
||||
return False
|
||||
|
||||
if stir and stir_speed > self._max_stir_speed:
|
||||
self.logger.error(f"Stir speed {stir_speed} exceeds maximum {self._max_stir_speed}")
|
||||
self.data["status"] = f"搅拌速度 {stir_speed} 超出范围"
|
||||
return False
|
||||
|
||||
# 开始加热/冷却
|
||||
self.data.update({
|
||||
"status": f"加热/冷却中: {vessel} 至 {temp}°C"
|
||||
})
|
||||
|
||||
# 模拟加热/冷却时间
|
||||
simulation_time = min(time, 10.0) # 最多等待10秒用于测试
|
||||
await asyncio.sleep(simulation_time)
|
||||
|
||||
# 加热/冷却完成
|
||||
self.data["status"] = f"完成: {vessel} 已达到 {temp}°C"
|
||||
|
||||
self.logger.info(f"HeatChill completed for vessel {vessel} at {temp}°C")
|
||||
return True
|
||||
|
||||
async def heat_chill_start(self, vessel: str, temp: float, purpose: str) -> bool:
|
||||
"""Start heat chill - matches HeatChillStart action exactly"""
|
||||
self.logger.info(f"HeatChillStart: vessel={vessel}, temp={temp}°C, purpose={purpose}")
|
||||
|
||||
# 验证参数
|
||||
if temp > self._max_temp or temp < self._min_temp:
|
||||
self.logger.error(f"Temperature {temp} outside range {self._min_temp}-{self._max_temp}")
|
||||
self.data["status"] = f"温度 {temp} 超出范围"
|
||||
return False
|
||||
|
||||
self.data["status"] = f"开始加热/冷却: {vessel} 至 {temp}°C"
|
||||
return True
|
||||
|
||||
async def heat_chill_stop(self, vessel: str) -> bool:
|
||||
"""Stop heat chill - matches HeatChillStop action exactly"""
|
||||
self.logger.info(f"HeatChillStop: vessel={vessel}")
|
||||
|
||||
self.data["status"] = f"停止加热/冷却: {vessel}"
|
||||
return True
|
||||
|
||||
# 状态属性 - 只保留 action 中定义的 feedback
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self.data.get("status", "Idle")
|
||||
197
unilabos/devices/virtual/virtual_pump.py
Normal file
197
unilabos/devices/virtual/virtual_pump.py
Normal 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")
|
||||
104
unilabos/devices/virtual/virtual_stirrer.py
Normal file
104
unilabos/devices/virtual/virtual_stirrer.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import asyncio
|
||||
import logging
|
||||
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 = {}
|
||||
|
||||
# 添加调试信息
|
||||
print(f"=== VirtualStirrer {self.device_id} is being created! ===")
|
||||
print(f"=== Config: {self.config} ===")
|
||||
print(f"=== Kwargs: {kwargs} ===")
|
||||
|
||||
# 从config或kwargs中获取配置参数
|
||||
self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL')
|
||||
self._max_temp = self.config.get('max_temp') or kwargs.get('max_temp', 100.0)
|
||||
self._max_speed = self.config.get('max_speed') or kwargs.get('max_speed', 1000.0)
|
||||
|
||||
# 处理其他kwargs参数,但跳过已知的配置参数
|
||||
skip_keys = {'port', 'max_temp', 'max_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"""
|
||||
print(f"=== VirtualStirrer {self.device_id} initialize() called! ===")
|
||||
self.logger.info(f"Initializing virtual stirrer {self.device_id}")
|
||||
self.data.update({
|
||||
"status": "Idle"
|
||||
})
|
||||
return True
|
||||
|
||||
async def cleanup(self) -> bool:
|
||||
"""Cleanup virtual stirrer"""
|
||||
self.logger.info(f"Cleaning up virtual stirrer {self.device_id}")
|
||||
return True
|
||||
|
||||
async def stir(self, stir_time: float, stir_speed: float, settling_time: float) -> bool:
|
||||
"""Execute stir action - matches Stir action exactly"""
|
||||
self.logger.info(f"Stir: speed={stir_speed} RPM, time={stir_time}s, settling={settling_time}s")
|
||||
|
||||
# 验证参数
|
||||
if stir_speed > self._max_speed:
|
||||
self.logger.error(f"Stir speed {stir_speed} exceeds maximum {self._max_speed}")
|
||||
self.data["status"] = f"搅拌速度 {stir_speed} 超出范围"
|
||||
return False
|
||||
|
||||
# 开始搅拌
|
||||
self.data["status"] = f"搅拌中: {stir_speed} RPM, {stir_time}s"
|
||||
|
||||
# 模拟搅拌时间
|
||||
simulation_time = min(stir_time, 10.0) # 最多等待10秒用于测试
|
||||
await asyncio.sleep(simulation_time)
|
||||
|
||||
# 搅拌完成,开始沉降
|
||||
if settling_time > 0:
|
||||
self.data["status"] = f"沉降中: {settling_time}s"
|
||||
settling_simulation = min(settling_time, 5.0) # 最多等待5秒
|
||||
await asyncio.sleep(settling_simulation)
|
||||
|
||||
# 操作完成
|
||||
self.data["status"] = "搅拌完成"
|
||||
|
||||
self.logger.info(f"Stir completed: {stir_speed} RPM for {stir_time}s")
|
||||
return True
|
||||
|
||||
async def start_stir(self, vessel: str, stir_speed: float, purpose: str) -> bool:
|
||||
"""Start stir action - matches StartStir action exactly"""
|
||||
self.logger.info(f"StartStir: vessel={vessel}, speed={stir_speed} RPM, purpose={purpose}")
|
||||
|
||||
# 验证参数
|
||||
if stir_speed > self._max_speed:
|
||||
self.logger.error(f"Stir speed {stir_speed} exceeds maximum {self._max_speed}")
|
||||
self.data["status"] = f"搅拌速度 {stir_speed} 超出范围"
|
||||
return False
|
||||
|
||||
self.data["status"] = f"开始搅拌: {vessel} at {stir_speed} RPM"
|
||||
return True
|
||||
|
||||
async def stop_stir(self, vessel: str) -> bool:
|
||||
"""Stop stir action - matches StopStir action exactly"""
|
||||
self.logger.info(f"StopStir: vessel={vessel}")
|
||||
|
||||
self.data["status"] = f"停止搅拌: {vessel}"
|
||||
return True
|
||||
|
||||
# 状态属性 - 只保留 action 中定义的 feedback
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self.data.get("status", "Idle")
|
||||
149
unilabos/devices/virtual/virtual_transferpump.py
Normal file
149
unilabos/devices/virtual/virtual_transferpump.py
Normal file
@@ -0,0 +1,149 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
class VirtualTransferPump:
|
||||
"""Virtual pump device specifically for Transfer 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_transfer_pump"
|
||||
self.config = config or {}
|
||||
|
||||
self.logger = logging.getLogger(f"VirtualTransferPump.{self.device_id}")
|
||||
self.data = {}
|
||||
|
||||
# 添加调试信息
|
||||
print(f"=== VirtualTransferPump {self.device_id} is being created! ===")
|
||||
print(f"=== Config: {self.config} ===")
|
||||
print(f"=== Kwargs: {kwargs} ===")
|
||||
|
||||
# 从config或kwargs中获取配置参数
|
||||
self.port = self.config.get('port') or kwargs.get('port', 'VIRTUAL')
|
||||
self._max_volume = self.config.get('max_volume') or kwargs.get('max_volume', 50.0)
|
||||
self._transfer_rate = self.config.get('transfer_rate') or kwargs.get('transfer_rate', 5.0)
|
||||
self._current_volume = 0.0
|
||||
self.is_running = False
|
||||
|
||||
async def initialize(self) -> bool:
|
||||
"""Initialize virtual transfer pump"""
|
||||
print(f"=== VirtualTransferPump {self.device_id} initialize() called! ===")
|
||||
self.logger.info(f"Initializing virtual transfer pump {self.device_id}")
|
||||
self.data.update({
|
||||
"status": "Idle",
|
||||
"current_volume": 0.0,
|
||||
"max_volume": self._max_volume,
|
||||
"transfer_rate": self._transfer_rate,
|
||||
"from_vessel": "",
|
||||
"to_vessel": "",
|
||||
"progress": 0.0,
|
||||
"transferred_volume": 0.0,
|
||||
"current_status": "Ready"
|
||||
})
|
||||
return True
|
||||
|
||||
async def cleanup(self) -> bool:
|
||||
"""Cleanup virtual transfer pump"""
|
||||
self.logger.info(f"Cleaning up virtual transfer pump {self.device_id}")
|
||||
return True
|
||||
|
||||
async def transfer(self, from_vessel: str, to_vessel: str, volume: float,
|
||||
amount: str = "", time: float = 0, viscous: bool = False,
|
||||
rinsing_solvent: str = "", rinsing_volume: float = 0.0,
|
||||
rinsing_repeats: int = 0, solid: bool = False) -> bool:
|
||||
"""Execute liquid transfer - matches Transfer action"""
|
||||
self.logger.info(f"Transfer: {volume}mL from {from_vessel} to {to_vessel}")
|
||||
|
||||
# 计算转移时间
|
||||
if time > 0:
|
||||
transfer_time = time
|
||||
else:
|
||||
# 如果是粘性液体,降低转移速率
|
||||
rate = self._transfer_rate * 0.5 if viscous else self._transfer_rate
|
||||
transfer_time = volume / rate
|
||||
|
||||
self.data.update({
|
||||
"status": "Running",
|
||||
"from_vessel": from_vessel,
|
||||
"to_vessel": to_vessel,
|
||||
"current_status": "Transferring",
|
||||
"progress": 0.0,
|
||||
"transferred_volume": 0.0
|
||||
})
|
||||
|
||||
# 模拟转移过程
|
||||
steps = 10
|
||||
step_time = transfer_time / steps
|
||||
step_volume = volume / steps
|
||||
|
||||
for i in range(steps):
|
||||
await asyncio.sleep(step_time)
|
||||
progress = (i + 1) / steps * 100
|
||||
transferred = (i + 1) * step_volume
|
||||
|
||||
self.data.update({
|
||||
"progress": progress,
|
||||
"transferred_volume": transferred,
|
||||
"current_status": f"Transferring {progress:.1f}%"
|
||||
})
|
||||
|
||||
self.logger.info(f"Transfer progress: {progress:.1f}% ({transferred:.1f}/{volume}mL)")
|
||||
|
||||
# 如果需要冲洗
|
||||
if rinsing_solvent and rinsing_volume > 0 and rinsing_repeats > 0:
|
||||
self.data["current_status"] = "Rinsing"
|
||||
for repeat in range(rinsing_repeats):
|
||||
self.logger.info(f"Rinsing cycle {repeat + 1}/{rinsing_repeats} with {rinsing_solvent}")
|
||||
await asyncio.sleep(1) # 模拟冲洗时间
|
||||
|
||||
self.data.update({
|
||||
"status": "Idle",
|
||||
"current_status": "Transfer completed",
|
||||
"progress": 100.0,
|
||||
"transferred_volume": volume
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
# 添加所有在virtual_device.yaml中定义的状态属性
|
||||
@property
|
||||
def status(self) -> str:
|
||||
return self.data.get("status", "Unknown")
|
||||
|
||||
@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", self._max_volume)
|
||||
|
||||
@property
|
||||
def transfer_rate(self) -> float:
|
||||
return self.data.get("transfer_rate", self._transfer_rate)
|
||||
|
||||
@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")
|
||||
105
unilabos/devices/virtual/virtual_valve.py
Normal file
105
unilabos/devices/virtual/virtual_valve.py
Normal 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)
|
||||
Reference in New Issue
Block a user