From ec4e6c6cfd040c094190f7ac33f8114f9206881e Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Fri, 23 May 2025 10:06:30 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E8=8B=B1=E6=96=87readme?= =?UTF-8?q?=E6=8F=8F=E8=BF=B0=20(#33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 67 ++++++++++++++++++++------------------- README_zh.md | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 124 insertions(+), 32 deletions(-) create mode 100644 README_zh.md diff --git a/README.md b/README.md index 0c1d9b1..02b6d06 100644 --- a/README.md +++ b/README.md @@ -4,83 +4,86 @@ # Uni-Lab-OS + +**English** | [中文](README_zh.md) + [![GitHub Stars](https://img.shields.io/github/stars/dptech-corp/Uni-Lab-OS.svg)](https://github.com/dptech-corp/Uni-Lab-OS/stargazers) [![GitHub Forks](https://img.shields.io/github/forks/dptech-corp/Uni-Lab-OS.svg)](https://github.com/dptech-corp/Uni-Lab-OS/network/members) [![GitHub Issues](https://img.shields.io/github/issues/dptech-corp/Uni-Lab-OS.svg)](https://github.com/dptech-corp/Uni-Lab-OS/issues) [![GitHub License](https://img.shields.io/github/license/dptech-corp/Uni-Lab-OS.svg)](https://github.com/dptech-corp/Uni-Lab-OS/blob/main/LICENSE) -Uni-Lab 操作系统是一个用于实验室自动化的综合平台,旨在连接和控制各种实验设备,实现实验流程的自动化和标准化。 +Uni-Lab Operating System is a platform for laboratory automation, designed to connect and control various experimental equipment, enabling automation and standardization of experimental workflows. -## 核心特点 +## Key Features -- 多设备集成管理 -- 自动化实验流程 -- 云端连接能力 -- 灵活的配置系统 -- 支持多种实验协议 +- Multi-device integration management +- Automated experimental workflows +- Cloud connectivity capabilities +- Flexible configuration system +- Support for multiple experimental protocols -## 文档 +## Documentation -详细文档可在以下位置找到: +Detailed documentation can be found at: -- [在线文档](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/) +- [Online Documentation](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/) -## 快速开始 +## Quick Start -1. 配置Conda环境 +1. Configure Conda Environment -Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的操作系统选择适当的环境文件: +Uni-Lab-OS recommends using `mamba` for environment management. Choose the appropriate environment file for your operating system: ```bash -# 创建新环境 +# Create new environment mamba env create -f unilabos-[YOUR_OS].yaml mamba activate unilab -# 或更新现有环境 -# 其中 `[YOUR_OS]` 可以是 `win64`, `linux-64`, `osx-64`, 或 `osx-arm64`。 -conda env update --file unilabos-[YOUR_OS].yml -n 环境名 +# Or update existing environment +# Where `[YOUR_OS]` can be `win64`, `linux-64`, `osx-64`, or `osx-arm64`. +conda env update --file unilabos-[YOUR_OS].yml -n environment_name -# 现阶段,需要安装 `unilabos_msgs` 包 -# 可以前往 Release 页面下载系统对应的包进行安装 +# Currently, you need to install the `unilabos_msgs` package +# You can download the system-specific package from the Release page conda install ros-humble-unilabos-msgs-0.9.0-xxxxx.tar.bz2 -# 安装PyLabRobot等前置 +# Install PyLabRobot and other prerequisites git clone https://github.com/PyLabRobot/pylabrobot plr_repo cd plr_repo pip install .[opentrons] ``` -2. 安装 Uni-Lab-OS: +2. Install Uni-Lab-OS: ```bash -# 克隆仓库 +# Clone the repository git clone https://github.com/dptech-corp/Uni-Lab-OS.git cd Uni-Lab-OS -# 安装 Uni-Lab-OS +# Install Uni-Lab-OS pip install . ``` -3. 启动 Uni-Lab 系统: +3. Start Uni-Lab System: -请见[文档-启动样例](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/boot_examples/index.html) +Please refer to [Documentation - Boot Examples](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/boot_examples/index.html) -## 消息格式 +## Message Format -Uni-Lab-OS 使用预构建的 `unilabos_msgs` 进行系统通信。您可以在 [GitHub Releases](https://github.com/dptech-corp/Uni-Lab-OS/releases) 页面找到已构建的版本。 +Uni-Lab-OS uses pre-built `unilabos_msgs` for system communication. You can find the built versions on the [GitHub Releases](https://github.com/dptech-corp/Uni-Lab-OS/releases) page. -## 许可证 +## License -此项目采用 GPL-3.0 许可 - 详情请参阅 [LICENSE](LICENSE) 文件。 +This project is licensed under GPL-3.0 - see the [LICENSE](LICENSE) file for details. -## 项目统计 +## Project Statistics -### Stars 趋势 +### Stars Trend Star History Chart -## 联系我们 +## Contact Us - GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues) \ No newline at end of file diff --git a/README_zh.md b/README_zh.md new file mode 100644 index 0000000..ce660cd --- /dev/null +++ b/README_zh.md @@ -0,0 +1,89 @@ +
+ Uni-Lab Logo +
+ +# Uni-Lab-OS + + +[English](README.md) | **中文** + +[![GitHub Stars](https://img.shields.io/github/stars/dptech-corp/Uni-Lab-OS.svg)](https://github.com/dptech-corp/Uni-Lab-OS/stargazers) +[![GitHub Forks](https://img.shields.io/github/forks/dptech-corp/Uni-Lab-OS.svg)](https://github.com/dptech-corp/Uni-Lab-OS/network/members) +[![GitHub Issues](https://img.shields.io/github/issues/dptech-corp/Uni-Lab-OS.svg)](https://github.com/dptech-corp/Uni-Lab-OS/issues) +[![GitHub License](https://img.shields.io/github/license/dptech-corp/Uni-Lab-OS.svg)](https://github.com/dptech-corp/Uni-Lab-OS/blob/main/LICENSE) + +Uni-Lab 操作系统是一个用于实验室自动化的综合平台,旨在连接和控制各种实验设备,实现实验流程的自动化和标准化。 + +## 核心特点 + +- 多设备集成管理 +- 自动化实验流程 +- 云端连接能力 +- 灵活的配置系统 +- 支持多种实验协议 + +## 文档 + +详细文档可在以下位置找到: + +- [在线文档](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/) + +## 快速开始 + +1. 配置Conda环境 + +Uni-Lab-OS 建议使用 `mamba` 管理环境。根据您的操作系统选择适当的环境文件: + +```bash +# 创建新环境 +mamba env create -f unilabos-[YOUR_OS].yaml +mamba activate unilab + +# 或更新现有环境 +# 其中 `[YOUR_OS]` 可以是 `win64`, `linux-64`, `osx-64`, 或 `osx-arm64`。 +conda env update --file unilabos-[YOUR_OS].yml -n 环境名 + +# 现阶段,需要安装 `unilabos_msgs` 包 +# 可以前往 Release 页面下载系统对应的包进行安装 +conda install ros-humble-unilabos-msgs-0.9.0-xxxxx.tar.bz2 + +# 安装PyLabRobot等前置 +git clone https://github.com/PyLabRobot/pylabrobot plr_repo +cd plr_repo +pip install .[opentrons] +``` + +2. 安装 Uni-Lab-OS: + +```bash +# 克隆仓库 +git clone https://github.com/dptech-corp/Uni-Lab-OS.git +cd Uni-Lab-OS + +# 安装 Uni-Lab-OS +pip install . +``` + +3. 启动 Uni-Lab 系统: + +请见[文档-启动样例](https://readthedocs.dp.tech/Uni-Lab/v0.8.0/boot_examples/index.html) + +## 消息格式 + +Uni-Lab-OS 使用预构建的 `unilabos_msgs` 进行系统通信。您可以在 [GitHub Releases](https://github.com/dptech-corp/Uni-Lab-OS/releases) 页面找到已构建的版本。 + +## 许可证 + +此项目采用 GPL-3.0 许可 - 详情请参阅 [LICENSE](LICENSE) 文件。 + +## 项目统计 + +### Stars 趋势 + + + Star History Chart + + +## 联系我们 + +- GitHub Issues: [https://github.com/dptech-corp/Uni-Lab-OS/issues](https://github.com/dptech-corp/Uni-Lab-OS/issues) \ No newline at end of file From bfcb214b5374973efbf0d93e2d0a6420fde264a4 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Thu, 29 May 2025 20:40:16 +0800 Subject: [PATCH 2/2] 24 high level liquidhandler (#28) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * unify liquid_handler definition * remove default values * Dev Sync (#25) * 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 --------- Co-authored-by: Harvey Que Co-authored-by: zhangshixiang <@zhangshixiang> Co-authored-by: q434343 <73513873+q434343@users.noreply.github.com> * remove default behavior for visualization * change liquidhandler name * resource create from outer easy * add easy resource creation * easy resource creation logic * remove wrongly debug msg from others * remove wrongly debug msg from others * add missing action clients * fix device_id * fix slot_on_deck * fix registry typo * complete require packages msg converter support array string implements create resource logic * 修复port输入 * fix: remove dirty actions --------- Co-authored-by: Junhan Chang Co-authored-by: Harvey Que Co-authored-by: q434343 <73513873+q434343@users.noreply.github.com> --- test/commands/resource_add.md | 2 +- unilabos-linux-64.yaml | 2 + unilabos-osx-64.yaml | 2 + unilabos-osx-arm64.yaml | 2 + unilabos-win64.yaml | 2 + unilabos/app/main.py | 8 +- unilabos/app/mq.py | 4 +- unilabos/devices/laiyu_add_solid/__init__.py | 0 ...finition.py => liquid_handler_abstract.py} | 2 +- unilabos/registry/devices/liquid_handler.yaml | 208 ++++++++---------- unilabos/registry/registry.py | 35 ++- .../resources/opentrons/reservoirs.yaml | 12 +- unilabos/resources/graphio.py | 11 +- unilabos/ros/msgs/message_converter.py | 9 +- unilabos/ros/nodes/base_device_node.py | 45 +++- unilabos/ros/nodes/presets/host_node.py | 54 ++++- unilabos_msgs/CMakeLists.txt | 15 +- .../action/DPLiquidHandlerCustomDelay.action | 6 - .../action/DPLiquidHandlerSetTiprack.action | 5 - .../action/DPLiquidHandlerTouchTip.action | 5 - .../DPLiquidHandlerTransferLiquid.action | 25 --- ...dLiquid.action => LiquidHandlerAdd.action} | 0 .../action/LiquidHandlerAspirate.action | 2 +- .../action/LiquidHandlerDispense.action | 2 +- ...dlerMix.action => LiquidHandlerMix.action} | 0 ...veTo.action => LiquidHandlerMoveTo.action} | 0 ...quid.action => LiquidHandlerRemove.action} | 0 .../action/LiquidHandlerTransfer.action | 29 ++- .../action/ResourceCreateFromOuterEasy.action | 12 + 29 files changed, 276 insertions(+), 223 deletions(-) create mode 100644 unilabos/devices/laiyu_add_solid/__init__.py rename unilabos/devices/liquid_handling/{action_definition.py => liquid_handler_abstract.py} (99%) delete mode 100644 unilabos_msgs/action/DPLiquidHandlerCustomDelay.action delete mode 100644 unilabos_msgs/action/DPLiquidHandlerSetTiprack.action delete mode 100644 unilabos_msgs/action/DPLiquidHandlerTouchTip.action delete mode 100644 unilabos_msgs/action/DPLiquidHandlerTransferLiquid.action rename unilabos_msgs/action/{DPLiquidHandlerAddLiquid.action => LiquidHandlerAdd.action} (100%) rename unilabos_msgs/action/{DPLiquidHandlerMix.action => LiquidHandlerMix.action} (100%) rename unilabos_msgs/action/{DPLiquidHandlerMoveTo.action => LiquidHandlerMoveTo.action} (100%) rename unilabos_msgs/action/{DPLiquidHandlerRemoveLiquid.action => LiquidHandlerRemove.action} (100%) create mode 100644 unilabos_msgs/action/ResourceCreateFromOuterEasy.action diff --git a/test/commands/resource_add.md b/test/commands/resource_add.md index 9d5fe38..d80e155 100644 --- a/test/commands/resource_add.md +++ b/test/commands/resource_add.md @@ -1,5 +1,5 @@ 使用plr_test.json启动,将Well加入Plate中 ```bash -ros2 action send_goal /devices/host_node/add_resource_from_outer unilabos_msgs/action/_resource_create_from_outer/ResourceCreateFromOuter "{ resources: [ { 'category': '', 'children': [], 'config': { 'type': 'Well', 'size_x': 6.86, 'size_y': 6.86, 'size_z': 10.67, 'rotation': { 'x': 0, 'y': 0, 'z': 0, 'type': 'Rotation' }, 'category': 'well', 'model': null, 'max_volume': 360, 'material_z_thickness': 0.5, 'compute_volume_from_height': null, 'compute_height_from_volume': null, 'bottom_type': 'flat', 'cross_section_type': 'circle' }, 'data': { 'liquids': [], 'pending_liquids': [], 'liquid_history': [] }, 'id': 'plate_well_11_7', 'name': 'plate_well_11_7', 'pose': { 'orientation': { 'w': 1.0, 'x': 0.0, 'y': 0.0, 'z': 0.0 }, 'position': { 'x': 0.0, 'y': 0.0, 'z': 0.0 } }, 'sample_id': '', 'parent': 'plate', 'type': 'device' } ], device_ids: [ 'PLR_STATION' ], bind_parent_ids: [ 'plate' ], bind_locations: [ { 'x': 0.0, 'y': 0.0, 'z': 0.0 } ], other_calling_params: [ '{}' ] }" +ros2 action send_goal /devices/host_node/create_resource_detailed unilabos_msgs/action/_resource_create_from_outer/ResourceCreateFromOuter "{ resources: [ { 'category': '', 'children': [], 'config': { 'type': 'Well', 'size_x': 6.86, 'size_y': 6.86, 'size_z': 10.67, 'rotation': { 'x': 0, 'y': 0, 'z': 0, 'type': 'Rotation' }, 'category': 'well', 'model': null, 'max_volume': 360, 'material_z_thickness': 0.5, 'compute_volume_from_height': null, 'compute_height_from_volume': null, 'bottom_type': 'flat', 'cross_section_type': 'circle' }, 'data': { 'liquids': [], 'pending_liquids': [], 'liquid_history': [] }, 'id': 'plate_well_11_7', 'name': 'plate_well_11_7', 'pose': { 'orientation': { 'w': 1.0, 'x': 0.0, 'y': 0.0, 'z': 0.0 }, 'position': { 'x': 0.0, 'y': 0.0, 'z': 0.0 } }, 'sample_id': '', 'parent': 'plate', 'type': 'device' } ], device_ids: [ 'PLR_STATION' ], bind_parent_ids: [ 'plate' ], bind_locations: [ { 'x': 0.0, 'y': 0.0, 'z': 0.0 } ], other_calling_params: [ '{}' ] }" ``` \ No newline at end of file diff --git a/unilabos-linux-64.yaml b/unilabos-linux-64.yaml index 3f5b91c..aeac636 100644 --- a/unilabos-linux-64.yaml +++ b/unilabos-linux-64.yaml @@ -56,6 +56,8 @@ dependencies: - ros-humble-moveit-servo # simulation - ros-humble-simulation + - ros-humble-tf-transformations + - transforms3d # ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo # ilab equipments # - ros-humble-unilabos-msgs diff --git a/unilabos-osx-64.yaml b/unilabos-osx-64.yaml index 38981f0..72ffb4c 100644 --- a/unilabos-osx-64.yaml +++ b/unilabos-osx-64.yaml @@ -56,6 +56,8 @@ dependencies: # - ros-humble-moveit-servo # simulation - ros-humble-simulation + - ros-humble-tf-transformations + - transforms3d # ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo # ilab equipments # - ros-humble-unilabos-msgs diff --git a/unilabos-osx-arm64.yaml b/unilabos-osx-arm64.yaml index 05333a3..83ffe1b 100644 --- a/unilabos-osx-arm64.yaml +++ b/unilabos-osx-arm64.yaml @@ -58,6 +58,8 @@ dependencies: - ros-humble-moveit-servo # simulation - ros-humble-simulation + - ros-humble-tf-transformations + - transforms3d # ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo # ilab equipments # - ros-humble-unilabos-msgs diff --git a/unilabos-win64.yaml b/unilabos-win64.yaml index 2e26fa3..19a4008 100644 --- a/unilabos-win64.yaml +++ b/unilabos-win64.yaml @@ -56,6 +56,8 @@ dependencies: - ros-humble-moveit-servo # simulation - ros-humble-simulation # ignored because of NO python3.11 package in WIN64 + - ros-humble-tf-transformations + - transforms3d # ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo # ilab equipments # ros-humble-unilabos-msgs diff --git a/unilabos/app/main.py b/unilabos/app/main.py index ebee015..0db290a 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -18,7 +18,6 @@ if unilabos_dir not in sys.path: from unilabos.config.config import load_config, BasicConfig, _update_config_from_env from unilabos.utils.banner_print import print_status, print_unilab_banner -from unilabos.device_mesh.resource_visalization import ResourceVisualization def parse_args(): @@ -188,11 +187,12 @@ def main(): if args_dict["visual"] != "disable": enable_rviz = args_dict["visual"] == "rviz" if devices_and_resources is not None: + from unilabos.device_mesh.resource_visalization import ResourceVisualization # 此处开启后,logger会变更为INFO,有需要请调整 resource_visualization = ResourceVisualization(devices_and_resources, args_dict["resources_config"] ,enable_rviz=enable_rviz) args_dict["resources_mesh_config"] = resource_visualization.resource_model start_backend(**args_dict) server_thread = threading.Thread(target=start_server, kwargs=dict( - open_browser=not args_dict["disable_browser"] + open_browser=not args_dict["disable_browser"], port=args_dict["port"], )) server_thread.start() asyncio.set_event_loop(asyncio.new_event_loop()) @@ -201,10 +201,10 @@ def main(): time.sleep(1) else: start_backend(**args_dict) - start_server(open_browser=not args_dict["disable_browser"]) + start_server(open_browser=not args_dict["disable_browser"], port=args_dict["port"],) else: start_backend(**args_dict) - start_server(open_browser=not args_dict["disable_browser"]) + start_server(open_browser=not args_dict["disable_browser"], port=args_dict["port"],) if __name__ == "__main__": diff --git a/unilabos/app/mq.py b/unilabos/app/mq.py index 018c65c..0bac96f 100644 --- a/unilabos/app/mq.py +++ b/unilabos/app/mq.py @@ -26,6 +26,7 @@ class MQTTClient: def __init__(self): self.mqtt_disable = not MQConfig.lab_id self.client_id = f"{MQConfig.group_id}@@@{MQConfig.lab_id}{uuid.uuid4()}" + logger.info("[MQTT] Client_id: " + self.client_id) self.client = mqtt.Client(CallbackAPIVersion.VERSION2, client_id=self.client_id, protocol=mqtt.MQTTv5) self._setup_callbacks() @@ -52,10 +53,7 @@ class MQTTClient: try: payload_str = msg.payload.decode("utf-8") payload_json = json.loads(payload_str) - logger.debug(f"Topic: {msg.topic}") - logger.debug("Payload:", json.dumps(payload_json, indent=2, ensure_ascii=False)) if msg.topic == f"labs/{MQConfig.lab_id}/job/start/": - logger.debug("job_add", type(payload_json), payload_json) if "data" not in payload_json: payload_json["data"] = {} if "action" in payload_json: diff --git a/unilabos/devices/laiyu_add_solid/__init__.py b/unilabos/devices/laiyu_add_solid/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unilabos/devices/liquid_handling/action_definition.py b/unilabos/devices/liquid_handling/liquid_handler_abstract.py similarity index 99% rename from unilabos/devices/liquid_handling/action_definition.py rename to unilabos/devices/liquid_handling/liquid_handler_abstract.py index 530703a..c349403 100644 --- a/unilabos/devices/liquid_handling/action_definition.py +++ b/unilabos/devices/liquid_handling/liquid_handler_abstract.py @@ -14,7 +14,7 @@ from pylabrobot.resources import ( Well ) -class DPLiquidHandler(LiquidHandler): +class LiquidHandlerAbstract(LiquidHandler): """Extended LiquidHandler with additional operations.""" # --------------------------------------------------------------- diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml index ba921c7..bcddae5 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -1,11 +1,96 @@ liquid_handler: description: Liquid handler device controlled by pylabrobot class: - module: pylabrobot.liquid_handling:LiquidHandler + module: unilabos.devices.liquid_handling.liquid_handler_abstract:LiquidHandlerAbstract type: python status_types: name: String action_value_mappings: + remove: + type: LiquidHandlerRemove + goal: + vols: vols + sources: sources + waste_liquid: waste_liquid + use_channels: use_channels + flow_rates: flow_rates + offsets: offsets + liquid_height: liquid_height + blow_out_air_volume: blow_out_air_volume + spread: spread + delays: delays + is_96_well: is_96_well + top: top + none_keys: none_keys + feedback: { } + result: { } + add_liquid: + type: LiquidHandlerAdd + goal: + asp_vols: asp_vols + dis_vols: dis_vols + reagent_sources: reagent_sources + targets: targets + use_channels: use_channels + flow_rates: flow_rates + offsets: offsets + liquid_height: liquid_height + blow_out_air_volume: blow_out_air_volume + spread: spread + is_96_well: is_96_well + mix_time: mix_time + mix_vol: mix_vol + mix_rate: mix_rate + mix_liquid_height: mix_liquid_height + none_keys: none_keys + feedback: { } + result: { } + transfer_liquid: + type: LiquidHandlerTransfer + goal: + asp_vols: asp_vols + dis_vols: dis_vols + sources: sources + targets: targets + tip_racks: tip_racks + use_channels: use_channels + asp_flow_rates: asp_flow_rates + dis_flow_rates: dis_flow_rates + offsets: offsets + touch_tip: touch_tip + liquid_height: liquid_height + blow_out_air_volume: blow_out_air_volume + spread: spread + is_96_well: is_96_well + mix_stage: mix_stage + mix_times: mix_times + mix_vol: mix_vol + mix_rate: mix_rate + mix_liquid_height: mix_liquid_height + delays: delays + none_keys: none_keys + feedback: { } + result: { } + mix: + type: LiquidHandlerMix + goal: + targets: targets + mix_time: mix_time + mix_vol: mix_vol + height_to_bottom: height_to_bottom + offsets: offsets + mix_rate: mix_rate + none_keys: none_keys + feedback: { } + result: { } + move_to: + type: LiquidHandlerMoveTo + goal: + well: well + dis_to_top: dis_to_top + channel: channel + feedback: { } + result: { } aspirate: type: LiquidHandlerAspirate goal: @@ -170,127 +255,6 @@ liquid_handler: - name additionalProperties: false -dp_liquid_handler: - description: 通用液体处理 - class: - module: unilabos.devices.liquid_handling.action_definition:DPLiquidHandler - type: python - status_types: - status: String - action_value_mappings: - remove_liquid: - type: DPLiquidHandlerRemoveLiquid - goal: - vols: vols - sources: sources - waste_liquid: waste_liquid - use_channels: use_channels - flow_rates: flow_rates - offsets: offsets - liquid_height: liquid_height - blow_out_air_volume: blow_out_air_volume - spread: spread - delays: delays - is_96_well: is_96_well - top: top - none_keys: none_keys - feedback: {} - result: {} - add_liquid: - type: DPLiquidHandlerAddLiquid - goal: - asp_vols: asp_vols - dis_vols: dis_vols - reagent_sources: reagent_sources - targets: targets - use_channels: use_channels - flow_rates: flow_rates - offsets: offsets - liquid_height: liquid_height - blow_out_air_volume: blow_out_air_volume - spread: spread - is_96_well: is_96_well - mix_time: mix_time - mix_vol: mix_vol - mix_rate: mix_rate - mix_liquid_height: mix_liquid_height - none_keys: none_keys - feedback: {} - result: {} - transfer_liquid: - type: DPLiquidHandlerTransferLiquid - goal: - asp_vols: asp_vols - dis_vols: dis_vols - sources: sources - targets: targets - tip_racks: tip_racks - use_channels: use_channels - asp_flow_rates: asp_flow_rates - dis_flow_rates: dis_flow_rates - offsets: offsets - touch_tip: touch_tip - liquid_height: liquid_height - blow_out_air_volume: blow_out_air_volume - spread: spread - is_96_well: is_96_well - mix_stage: mix_stage - mix_times: mix_times - mix_vol: mix_vol - mix_rate: mix_rate - mix_liquid_height: mix_liquid_height - delays: delays - none_keys: none_keys - feedback: {} - result: {} - custom_delay: - type: DPLiquidHandlerCustomDelay - goal: - seconds: seconds - msg: msg - feedback: {} - result: {} - touch_tip: - type: DPLiquidHandlerTouchTip - goal: - targets: targets - feedback: {} - result: {} - mix: - type: DPLiquidHandlerMix - goal: - targets: targets - mix_time: mix_time - mix_vol: mix_vol - height_to_bottom: height_to_bottom - offsets: offsets - mix_rate: mix_rate - none_keys: none_keys - feedback: {} - result: {} - set_tiprack: - type: DPLiquidHandlerSetTiprack - goal: - tip_racks: tip_racks - feedback: {} - result: {} - move_to: - type: DPLiquidHandlerMoveTo - goal: - well: well - dis_to_top: dis_to_top - channel: channel - feedback: {} - result: {} - schema: - type: object - properties: - name: - type: string - description: 物料名 - required: - - name - liquid_handler.revvity: class: module: unilabos.devices.liquid_handling.revvity:Revvity diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py index 6381e02..91789b7 100644 --- a/unilabos/registry/registry.py +++ b/unilabos/registry/registry.py @@ -1,5 +1,4 @@ import io -import json import os import sys from pathlib import Path @@ -7,10 +6,9 @@ from typing import Any import yaml -from unilabos.utils import logger from unilabos.ros.msgs.message_converter import msg_converter_manager, ros_action_to_json_schema +from unilabos.utils import logger from unilabos.utils.decorator import singleton -from unilabos.utils.type_check import TypeEncoder DEFAULT_PATHS = [Path(__file__).absolute().parent] @@ -21,10 +19,12 @@ class Registry: self.registry_paths = DEFAULT_PATHS.copy() # 使用copy避免修改默认值 if registry_paths: self.registry_paths.extend(registry_paths) - action_type = self._replace_type_with_class( - "ResourceCreateFromOuter", "host_node", f"动作 add_resource_from_outer" + self.ResourceCreateFromOuter = self._replace_type_with_class( + "ResourceCreateFromOuter", "host_node", f"动作 create_resource_detailed" + ) + self.ResourceCreateFromOuterEasy = self._replace_type_with_class( + "ResourceCreateFromOuterEasy", "host_node", f"动作 create_resource" ) - schema = ros_action_to_json_schema(action_type) self.device_type_registry = { "host_node": { "description": "UniLabOS主机节点", @@ -33,7 +33,7 @@ class Registry: "type": "python", "status_types": {}, "action_value_mappings": { - "add_resource_from_outer": { + "create_resource_detailed": { "type": msg_converter_manager.search_class("ResourceCreateFromOuter"), "goal": { "resources": "resources", @@ -46,7 +46,26 @@ class Registry: "result": { "success": "success" }, - "schema": schema + "schema": ros_action_to_json_schema(self.ResourceCreateFromOuter) + }, + "create_resource": { + "type": msg_converter_manager.search_class("ResourceCreateFromOuterEasy"), + "goal": { + "res_id": "res_id", + "class_name": "class_name", + "parent": "parent", + "device_id": "device_id", + "bind_locations": "bind_locations", + "liquid_input_slot": "liquid_input_slot[]", + "liquid_type": "liquid_type[]", + "liquid_volume": "liquid_volume[]", + "slot_on_deck": "slot_on_deck", + }, + "feedback": {}, + "result": { + "success": "success" + }, + "schema": ros_action_to_json_schema(self.ResourceCreateFromOuterEasy) } } }, diff --git a/unilabos/registry/resources/opentrons/reservoirs.yaml b/unilabos/registry/resources/opentrons/reservoirs.yaml index f966f0b..fbc8490 100644 --- a/unilabos/registry/resources/opentrons/reservoirs.yaml +++ b/unilabos/registry/resources/opentrons/reservoirs.yaml @@ -1,35 +1,35 @@ agilent_1_reservoir_290ml: description: Agilent 1 reservoir 290ml class: - module: pylabrobot.resources.opentrons.reserviors:agilent_1_reservoir_290ml + module: pylabrobot.resources.opentrons.reservoirs:agilent_1_reservoir_290ml type: pylabrobot axygen_1_reservoir_90ml: description: Axygen 1 reservoir 90ml class: - module: pylabrobot.resources.opentrons.reserviors:axygen_1_reservoir_90ml + module: pylabrobot.resources.opentrons.reservoirs:axygen_1_reservoir_90ml type: pylabrobot nest_12_reservoir_15ml: description: Nest 12 reservoir 15ml class: - module: pylabrobot.resources.opentrons.reserviors:nest_12_reservoir_15ml + module: pylabrobot.resources.opentrons.reservoirs:nest_12_reservoir_15ml type: pylabrobot nest_1_reservoir_195ml: description: Nest 1 reservoir 195ml class: - module: pylabrobot.resources.opentrons.reserviors:nest_1_reservoir_195ml + module: pylabrobot.resources.opentrons.reservoirs:nest_1_reservoir_195ml type: pylabrobot nest_1_reservoir_290ml: description: Nest 1 reservoir 290ml class: - module: pylabrobot.resources.opentrons.reserviors:nest_1_reservoir_290ml + module: pylabrobot.resources.opentrons.reservoirs:nest_1_reservoir_290ml type: pylabrobot usascientific_12_reservoir_22ml: description: USAScientific 12 reservoir 22ml class: - module: pylabrobot.resources.opentrons.reserviors:usascientific_12_reservoir_22ml + module: pylabrobot.resources.opentrons.reservoirs:usascientific_12_reservoir_22ml type: pylabrobot diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index 9621b5d..9a31add 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -189,6 +189,7 @@ def dict_from_graph(graph: nx.Graph) -> dict: def dict_to_tree(nodes: dict, devices_only: bool = False) -> list[dict]: # 将节点转换为字典,以便通过 ID 快速查找 nodes_list = [node for node in nodes.values() if node.get("type") == "device" or not devices_only] + id_list = [node["id"] for node in nodes_list] # 初始化每个节点的 children 为包含节点字典的列表 for node in nodes_list: @@ -196,7 +197,7 @@ def dict_to_tree(nodes: dict, devices_only: bool = False) -> list[dict]: # 找到根节点并返回 root_nodes = [ - node for node in nodes_list if len(nodes_list) == 1 or node.get("parent", node.get("parent_name")) in [None, "", "None", np.nan] + node for node in nodes_list if len(nodes_list) == 1 or node.get("parent", node.get("parent_name")) in [None, "", "None", np.nan] or node.get("parent", node.get("parent_name")) not in id_list ] # 如果存在多个根节点,返回所有根节点 @@ -421,7 +422,7 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None): return r -def initialize_resource(resource_config: dict, lab_registry: dict) -> list[dict]: +def initialize_resource(resource_config: dict) -> list[dict]: """Initializes a resource based on its configuration. If the config is detailed, then do nothing; @@ -433,6 +434,7 @@ def initialize_resource(resource_config: dict, lab_registry: dict) -> list[dict] Returns: None """ + from unilabos.registry.registry import lab_registry resource_class_config = resource_config.get("class", None) if resource_class_config is None: return [resource_config] @@ -476,11 +478,8 @@ def initialize_resources(resources_config) -> list[dict]: None """ - from unilabos.registry.registry import lab_registry resources = [] for resource_config in resources_config: - if resource_config["parent"] == "tip_rack" or resource_config["parent"] == "plate_well": - continue - resources.extend(initialize_resource(resource_config, lab_registry)) + resources.extend(initialize_resource(resource_config)) return resources diff --git a/unilabos/ros/msgs/message_converter.py b/unilabos/ros/msgs/message_converter.py index 4f85a11..11c7afd 100644 --- a/unilabos/ros/msgs/message_converter.py +++ b/unilabos/ros/msgs/message_converter.py @@ -348,10 +348,16 @@ def convert_to_ros_msg(ros_msg_type: Union[Type, Any], obj: Any) -> Any: if isinstance(td, NamespacedType): target_class = msg_converter_manager.get_class(f"{'.'.join(td.namespaces)}.{td.name}") setattr(ros_msg, key, [convert_to_ros_msg(target_class, v) for v in value]) + elif isinstance(td, UnboundedString): + setattr(ros_msg, key, value) else: + logger.warning(f"Not Supported type: {td}") setattr(ros_msg, key, []) # FIXME elif "array.array" in str(type(attr)): - setattr(ros_msg, key, value) + if attr.typecode == "f": + setattr(ros_msg, key, [float(i) for i in value]) + else: + setattr(ros_msg, key, value) else: nested_ros_msg = convert_to_ros_msg(type(attr)(), value) setattr(ros_msg, key, nested_ros_msg) @@ -574,6 +580,7 @@ basic_type_map = { 'int64': {'type': 'integer'}, 'uint64': {'type': 'integer', 'minimum': 0}, 'double': {'type': 'number'}, + 'float': {'type': 'number'}, 'float32': {'type': 'number'}, 'float64': {'type': 'number'}, 'string': {'type': 'string'}, diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index f79dddc..28b67aa 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -1,3 +1,4 @@ +import copy import functools import json import threading @@ -20,7 +21,7 @@ from unilabos_msgs.action import SendCmd from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response from unilabos.resources.graphio import convert_resources_to_type, convert_resources_from_type, resource_ulab_to_plr, \ - initialize_resources + initialize_resources, list_to_nested_dict, dict_to_tree, resource_plr_to_ulab, tree_to_list from unilabos.ros.msgs.message_converter import ( convert_to_ros_msg, convert_from_ros_msg, @@ -312,7 +313,10 @@ class BaseROS2DeviceNode(Node, Generic[T]): # 物料传输到对应的node节点 rclient = self.create_client(ResourceAdd, "/resources/add") rclient.wait_for_service() + rclient2 = self.create_client(ResourceAdd, "/resources/add") + rclient2.wait_for_service() request = ResourceAdd.Request() + request2 = ResourceAdd.Request() command_json = json.loads(req.command) namespace = command_json["namespace"] bind_parent_id = command_json["bind_parent_id"] @@ -321,11 +325,23 @@ class BaseROS2DeviceNode(Node, Generic[T]): other_calling_param = command_json["other_calling_param"] resources = command_json["resource"] initialize_full = other_calling_param.pop("initialize_full", False) + # 用来增加液体 + ADD_LIQUID_TYPE = other_calling_param.pop("ADD_LIQUID_TYPE", []) + LIQUID_VOLUME = other_calling_param.pop("LIQUID_VOLUME", []) + LIQUID_INPUT_SLOT = other_calling_param.pop("LIQUID_INPUT_SLOT", []) + slot = other_calling_param.pop("slot", -1) + if slot >= 0: # slot为负数的时候采用assign方法 + other_calling_param["slot"] = slot # 本地拿到这个物料,可能需要先做初始化? if isinstance(resources, list): - if initialize_full: + if len(resources) == 1 and isinstance(resources[0], list) and not initialize_full: # 取消,不存在的情况 + # 预先initialize过,以整组的形式传入 + request.resources = [convert_to_ros_msg(Resource, resource_) for resource_ in resources[0]] + elif initialize_full: resources = initialize_resources(resources) - request.resources = [convert_to_ros_msg(Resource, resource) for resource in resources] + request.resources = [convert_to_ros_msg(Resource, resource) for resource in resources] + else: + request.resources = [convert_to_ros_msg(Resource, resource) for resource in resources] else: if initialize_full: resources = initialize_resources([resources]) @@ -335,20 +351,31 @@ class BaseROS2DeviceNode(Node, Generic[T]): res.response = "OK" # 接下来该根据bind_parent_id进行assign了,目前只有plr可以进行assign,不然没有办法输入到物料系统中 resource = self.resource_tracker.figure_resource({"name": bind_parent_id}) - request.resources = [convert_to_ros_msg(Resource, resources)] + # request.resources = [convert_to_ros_msg(Resource, resources)] try: from pylabrobot.resources.resource import Resource as ResourcePLR from pylabrobot.resources.deck import Deck from pylabrobot.resources import Coordinate from pylabrobot.resources import OTDeck + from pylabrobot.resources import Plate contain_model = not isinstance(resource, Deck) if isinstance(resource, ResourcePLR): # resources.list() - plr_instance = resource_ulab_to_plr(resources, contain_model) + resources_tree = dict_to_tree(copy.deepcopy({r["id"]: r for r in resources})) + plr_instance = resource_ulab_to_plr(resources_tree[0], contain_model) + if isinstance(plr_instance, Plate): + empty_liquid_info_in = [(None, 0)] * plr_instance.num_items + for liquid_type, liquid_volume, liquid_input_slot in zip(ADD_LIQUID_TYPE, LIQUID_VOLUME, LIQUID_INPUT_SLOT): + empty_liquid_info_in[liquid_input_slot] = (liquid_type, liquid_volume) + plr_instance.set_well_liquids(empty_liquid_info_in) if isinstance(resource, OTDeck) and "slot" in other_calling_param: resource.assign_child_at_slot(plr_instance, **other_calling_param) - resource.assign_child_resource(plr_instance, Coordinate(location["x"], location["y"], location["z"]), **other_calling_param) + else: + _discard_slot = other_calling_param.pop("slot", -1) + resource.assign_child_resource(plr_instance, Coordinate(location["x"], location["y"], location["z"]), **other_calling_param) + request2.resources = [convert_to_ros_msg(Resource, r) for r in tree_to_list([resource_plr_to_ulab(resource)])] + rclient2.call(request2) # 发送给ResourceMeshManager action_client = ActionClient( self, SendCmd, "/devices/resource_mesh_manager/add_resource_mesh", callback_group=self.callback_group @@ -526,7 +553,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): action_kwargs = convert_from_ros_msg_with_mapping(goal, action_value_mapping["goal"]) self.lab_logger().debug(f"接收到原始目标: {action_kwargs}") # 向Host查询物料当前状态,如果是host本身的增加物料的请求,则直接跳过 - if action_name != "add_resource_from_outer": + if action_name not in ["create_resource_detailed", "create_resource"]: for k, v in goal.get_fields_and_field_types().items(): if v in ["unilabos_msgs/Resource", "sequence"]: self.lab_logger().info(f"查询资源状态: Key: {k} Type: {v}") @@ -625,7 +652,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): del future # 向Host更新物料当前状态 - if action_name != "add_resource_from_outer": + if action_name not in ["create_resource_detailed", "create_resource"]: for k, v in goal.get_fields_and_field_types().items(): if v not in ["unilabos_msgs/Resource", "sequence"]: continue @@ -764,7 +791,7 @@ class ROS2DeviceNode: self.resource_tracker = DeviceNodeResourceTracker() # use_pylabrobot_creator 使用 cls的包路径检测 - use_pylabrobot_creator = driver_class.__module__.startswith("pylabrobot") or driver_class.__name__ == "DPLiquidHandler" + use_pylabrobot_creator = driver_class.__module__.startswith("pylabrobot") or driver_class.__name__ == "LiquidHandlerAbstract" # TODO: 要在创建之前预先请求服务器是否有当前id的物料,放到resource_tracker中,让pylabrobot进行创建 # 创建设备类实例 diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index cb1e1c6..a8a3299 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -17,6 +17,7 @@ from unilabos_msgs.srv import ResourceAdd, ResourceGet, ResourceDelete, Resource from unique_identifier_msgs.msg import UUID from unilabos.registry.registry import lab_registry +from unilabos.resources.graphio import initialize_resource from unilabos.resources.registry import add_schema from unilabos.ros.initialize_device import initialize_device_from_dict from unilabos.ros.msgs.message_converter import ( @@ -100,7 +101,14 @@ class HostNode(BaseROS2DeviceNode): self.devices_names: Dict[str, str] = {device_id: self.namespace} # 存储设备名称和命名空间的映射 self.devices_instances: Dict[str, ROS2DeviceNode] = {} # 存储设备实例 self.device_machine_names: Dict[str, str] = {device_id: "本地", } # 存储设备ID到机器名称的映射 - self._action_clients: Dict[str, ActionClient] = {} # 用来存储多个ActionClient实例 + self._action_clients: Dict[str, ActionClient] = { # 为了方便了解实际的数据类型,host的默认写好 + "/devices/host_node/create_resource": ActionClient( + self, lab_registry.ResourceCreateFromOuterEasy, "/devices/host_node/create_resource", callback_group=self.callback_group + ), + "/devices/host_node/create_resource_detailed": ActionClient( + self, lab_registry.ResourceCreateFromOuter, "/devices/host_node/create_resource_detailed", callback_group=self.callback_group + ) + } # 用来存储多个ActionClient实例 self._action_value_mappings: Dict[str, Dict] = {} # 用来存储多个ActionClient的type, goal, feedback, result的变量名映射关系 self._goals: Dict[str, Any] = {} # 用来存储多个目标的状态 self._online_devices: Set[str] = {f"{self.namespace}/{device_id}"} # 用于跟踪在线设备 @@ -141,6 +149,21 @@ class HostNode(BaseROS2DeviceNode): ].items(): controller_config["update_rate"] = update_rate self.initialize_controller(controller_id, controller_config) + resources_config.insert(0, { + "id": "host_node", + "name": "host_node", + "parent": None, + "type": "device", + "class": "host_node", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": {}, + "data": {}, + "children": [] + }) resource_with_parent_name = [] resource_ids_to_instance = {i["id"]: i for i in resources_config} for res in resources_config: @@ -278,7 +301,7 @@ class HostNode(BaseROS2DeviceNode): except Exception as e: self.lab_logger().error(f"[Host Node] Failed to create ActionClient for {action_id}: {str(e)}") - def add_resource_from_outer(self, resources: list["Resource"], device_ids: list[str], bind_parent_ids: list[str], bind_locations: list[Point], other_calling_params: list[str]): + def create_resource_detailed(self, resources: list["Resource"], device_ids: list[str], bind_parent_ids: list[str], bind_locations: list[Point], other_calling_params: list[str]): for resource, device_id, bind_parent_id, bind_location, other_calling_param in zip(resources, device_ids, bind_parent_ids, bind_locations, other_calling_params): # 这里要求device_id传入必须是edge_device_id namespace = "/devices/" + device_id @@ -287,7 +310,7 @@ class HostNode(BaseROS2DeviceNode): sclient.wait_for_service() request = SerialCommand.Request() request.command = json.dumps({ - "resource": resource, + "resource": resource, # 单个/单组 可为 list[list[Resource]] "namespace": namespace, "edge_device_id": device_id, "bind_parent_id": bind_parent_id, @@ -302,6 +325,31 @@ class HostNode(BaseROS2DeviceNode): pass pass + def create_resource(self, device_id: str, res_id: str, class_name: str, parent: str, bind_locations: Point, liquid_input_slot: list[int], liquid_type: list[str], liquid_volume: list[int], slot_on_deck: int): + init_new_res = initialize_resource({ + "name": res_id, + "class": class_name, + "parent": parent, + "position": { + "x": bind_locations.x, + "y": bind_locations.y, + "z": bind_locations.z, + } + }) # flatten的格式 + resources = [init_new_res] + device_id = [device_id] + bind_parent_id = [parent] + bind_location = [bind_locations] + other_calling_param = [json.dumps({ + "ADD_LIQUID_TYPE": liquid_type, + "LIQUID_VOLUME": liquid_volume, + "LIQUID_INPUT_SLOT": liquid_input_slot, + "initialize_full": False, + "slot": slot_on_deck + })] + + return self.create_resource_detailed(resources, device_id, bind_parent_id, bind_location, other_calling_param) + def initialize_device(self, device_id: str, device_config: Dict[str, Any]) -> None: """ 根据配置初始化设备, diff --git a/unilabos_msgs/CMakeLists.txt b/unilabos_msgs/CMakeLists.txt index 69fbaa3..1ca6722 100644 --- a/unilabos_msgs/CMakeLists.txt +++ b/unilabos_msgs/CMakeLists.txt @@ -43,14 +43,10 @@ set(action_files "action/LiquidHandlerStamp.action" "action/LiquidHandlerTransfer.action" - "action/DPLiquidHandlerAddLiquid.action" - "action/DPLiquidHandlerCustomDelay.action" - "action/DPLiquidHandlerMix.action" - "action/DPLiquidHandlerMoveTo.action" - "action/DPLiquidHandlerRemoveLiquid.action" - "action/DPLiquidHandlerSetTiprack.action" - "action/DPLiquidHandlerTouchTip.action" - "action/DPLiquidHandlerTransferLiquid.action" + "action/LiquidHandlerAdd.action" + "action/LiquidHandlerMix.action" + "action/LiquidHandlerMoveTo.action" + "action/LiquidHandlerRemove.action" "action/EmptyIn.action" "action/FloatSingleInput.action" @@ -59,9 +55,10 @@ set(action_files "action/Point3DSeparateInput.action" "action/ResourceCreateFromOuter.action" + "action/ResourceCreateFromOuterEasy.action" "action/SolidDispenseAddPowderTube.action" - + "action/PumpTransfer.action" "action/Clean.action" "action/Separate.action" diff --git a/unilabos_msgs/action/DPLiquidHandlerCustomDelay.action b/unilabos_msgs/action/DPLiquidHandlerCustomDelay.action deleted file mode 100644 index 29f9b45..0000000 --- a/unilabos_msgs/action/DPLiquidHandlerCustomDelay.action +++ /dev/null @@ -1,6 +0,0 @@ -float64 seconds -string msg ---- -bool success ---- -# 反馈 \ No newline at end of file diff --git a/unilabos_msgs/action/DPLiquidHandlerSetTiprack.action b/unilabos_msgs/action/DPLiquidHandlerSetTiprack.action deleted file mode 100644 index 437d3e3..0000000 --- a/unilabos_msgs/action/DPLiquidHandlerSetTiprack.action +++ /dev/null @@ -1,5 +0,0 @@ -Resource[] tip_racks ---- -bool success ---- -# 反馈 \ No newline at end of file diff --git a/unilabos_msgs/action/DPLiquidHandlerTouchTip.action b/unilabos_msgs/action/DPLiquidHandlerTouchTip.action deleted file mode 100644 index e0c3104..0000000 --- a/unilabos_msgs/action/DPLiquidHandlerTouchTip.action +++ /dev/null @@ -1,5 +0,0 @@ -Resource[] targets ---- -bool success ---- -# 反馈 \ No newline at end of file diff --git a/unilabos_msgs/action/DPLiquidHandlerTransferLiquid.action b/unilabos_msgs/action/DPLiquidHandlerTransferLiquid.action deleted file mode 100644 index 39df59b..0000000 --- a/unilabos_msgs/action/DPLiquidHandlerTransferLiquid.action +++ /dev/null @@ -1,25 +0,0 @@ -float64[] asp_vols -float64[] dis_vols -Resource[] sources -Resource[] targets -Resource[] tip_racks -int32[] use_channels -float64[] asp_flow_rates -float64[] dis_flow_rates -geometry_msgs/Point[] offsets -bool touch_tip -float64[] liquid_height -float64[] blow_out_air_volume -string spread -bool is_96_well -string mix_stage -int32[] mix_times -int32 mix_vol -int32 mix_rate -float64 mix_liquid_height -int32[] delays -string[] none_keys ---- -bool success ---- -# 反馈 \ No newline at end of file diff --git a/unilabos_msgs/action/DPLiquidHandlerAddLiquid.action b/unilabos_msgs/action/LiquidHandlerAdd.action similarity index 100% rename from unilabos_msgs/action/DPLiquidHandlerAddLiquid.action rename to unilabos_msgs/action/LiquidHandlerAdd.action diff --git a/unilabos_msgs/action/LiquidHandlerAspirate.action b/unilabos_msgs/action/LiquidHandlerAspirate.action index f03ad07..9ba1706 100644 --- a/unilabos_msgs/action/LiquidHandlerAspirate.action +++ b/unilabos_msgs/action/LiquidHandlerAspirate.action @@ -5,7 +5,7 @@ float64[] flow_rates geometry_msgs/Point[] offsets float64[] liquid_height float64[] blow_out_air_volume -string spread="wide" +string spread --- bool success --- \ No newline at end of file diff --git a/unilabos_msgs/action/LiquidHandlerDispense.action b/unilabos_msgs/action/LiquidHandlerDispense.action index ba5360a..73c4d0f 100644 --- a/unilabos_msgs/action/LiquidHandlerDispense.action +++ b/unilabos_msgs/action/LiquidHandlerDispense.action @@ -5,7 +5,7 @@ int32[] use_channels float64[] flow_rates geometry_msgs/Point[] offsets int32[] blow_out_air_volume -string spread="wide" +string spread --- # 结果字段 bool success diff --git a/unilabos_msgs/action/DPLiquidHandlerMix.action b/unilabos_msgs/action/LiquidHandlerMix.action similarity index 100% rename from unilabos_msgs/action/DPLiquidHandlerMix.action rename to unilabos_msgs/action/LiquidHandlerMix.action diff --git a/unilabos_msgs/action/DPLiquidHandlerMoveTo.action b/unilabos_msgs/action/LiquidHandlerMoveTo.action similarity index 100% rename from unilabos_msgs/action/DPLiquidHandlerMoveTo.action rename to unilabos_msgs/action/LiquidHandlerMoveTo.action diff --git a/unilabos_msgs/action/DPLiquidHandlerRemoveLiquid.action b/unilabos_msgs/action/LiquidHandlerRemove.action similarity index 100% rename from unilabos_msgs/action/DPLiquidHandlerRemoveLiquid.action rename to unilabos_msgs/action/LiquidHandlerRemove.action diff --git a/unilabos_msgs/action/LiquidHandlerTransfer.action b/unilabos_msgs/action/LiquidHandlerTransfer.action index b6e3be3..d815a6c 100644 --- a/unilabos_msgs/action/LiquidHandlerTransfer.action +++ b/unilabos_msgs/action/LiquidHandlerTransfer.action @@ -1,11 +1,26 @@ # Bio -Resource source +float64[] asp_vols +float64[] dis_vols +Resource[] sources Resource[] targets -float64 source_vol -float64[] ratios -float64[] target_vols -float64 aspiration_flow_rate -float64[] dispense_flow_rates +Resource[] tip_racks +int32[] use_channels +float64[] asp_flow_rates +float64[] dis_flow_rates +geometry_msgs/Point[] offsets +bool touch_tip +float64[] liquid_height +float64[] blow_out_air_volume +string spread +bool is_96_well +string mix_stage +int32[] mix_times +int32 mix_vol +int32 mix_rate +float64 mix_liquid_height +int32[] delays +string[] none_keys --- bool success ---- \ No newline at end of file +--- +# 反馈 \ No newline at end of file diff --git a/unilabos_msgs/action/ResourceCreateFromOuterEasy.action b/unilabos_msgs/action/ResourceCreateFromOuterEasy.action new file mode 100644 index 0000000..cc832a7 --- /dev/null +++ b/unilabos_msgs/action/ResourceCreateFromOuterEasy.action @@ -0,0 +1,12 @@ +string res_id +string device_id +string class_name +string parent +geometry_msgs/Point bind_locations +int32[] liquid_input_slot +string[] liquid_type +float32[] liquid_volume +int32 slot_on_deck +--- +bool success +---