diff --git a/README.md b/README.md index b3256575..0c1d9b11 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,11 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名 # 现阶段,需要安装 `unilabos_msgs` 包 # 可以前往 Release 页面下载系统对应的包进行安装 -conda install ros-humble-unilabos-msgs-0.8.0-xxxxx.tar.bz2 +conda install ros-humble-unilabos-msgs-0.9.0-xxxxx.tar.bz2 # 安装PyLabRobot等前置 -git clone https://github.com/PyLabRobot/pylabrobot +git clone https://github.com/PyLabRobot/pylabrobot plr_repo +cd plr_repo pip install .[opentrons] ``` diff --git a/recipes/ros-humble-unilabos-msgs/recipe.yaml b/recipes/ros-humble-unilabos-msgs/recipe.yaml index db9c3ede..b6997113 100644 --- a/recipes/ros-humble-unilabos-msgs/recipe.yaml +++ b/recipes/ros-humble-unilabos-msgs/recipe.yaml @@ -1,6 +1,6 @@ package: name: ros-humble-unilabos-msgs - version: 0.8.0 + version: 0.9.0 source: path: ../../unilabos_msgs folder: ros-humble-unilabos-msgs/src/work diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml index 4fec1c02..4840bd65 100644 --- a/recipes/unilabos/recipe.yaml +++ b/recipes/unilabos/recipe.yaml @@ -1,6 +1,6 @@ package: name: unilabos - version: "0.8.0" + version: "0.9.0" source: path: ../.. diff --git a/setup.py b/setup.py index 5e29ee8b..5c06a7d8 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ package_name = 'unilabos' setup( name=package_name, - version='0.8.0', + version='0.9.0', packages=find_packages(), include_package_data=True, install_requires=['setuptools'], diff --git a/test/commands/resource_add.md b/test/commands/resource_add.md new file mode 100644 index 00000000..9d5fe38f --- /dev/null +++ b/test/commands/resource_add.md @@ -0,0 +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: [ '{}' ] }" +``` \ No newline at end of file diff --git a/test/experiments/plr_test.json b/test/experiments/plr_test.json index c60f1b54..7fe1e3e3 100644 --- a/test/experiments/plr_test.json +++ b/test/experiments/plr_test.json @@ -6679,8 +6679,7 @@ "plate_well_11_3", "plate_well_11_4", "plate_well_11_5", - "plate_well_11_6", - "plate_well_11_7" + "plate_well_11_6" ], "parent": "deck", "type": "device", @@ -10508,45 +10507,6 @@ "pending_liquids": [], "liquid_history": [] } - }, - { - "id": "plate_well_11_7", - "name": "plate_well_11_7", - "sample_id": null, - "children": [], - "parent": "plate", - "type": "device", - "class": "", - "position": { - "x": 109.87, - "y": 7.77, - "z": 3.03 - }, - "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": [] - } } ], "links": [] diff --git a/test/experiments/test_copy.json b/test/experiments/test_copy.json index df40affc..9b1debed 100644 --- a/test/experiments/test_copy.json +++ b/test/experiments/test_copy.json @@ -62,6 +62,7 @@ "name": "teaching_carrier", "sample_id": null, "children": [ + "teaching_carrier_A1" ], "parent": "deck", "type": "plate", @@ -86,6 +87,46 @@ "model": null }, "data": {} + }, + { + "id": "teaching_carrier_A1", + "name": "teaching_carrier_A1", + "sample_id": null, + "children": [], + "parent": "teaching_carrier", + "type": "device", + "class": "", + "position": { + "x": 10.87, + "y": 70.77, + "z": 10 + }, + "config": { + "type": "TipSpot", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } } ], "links": [ diff --git a/unilabos-linux-64.yaml b/unilabos-linux-64.yaml index 7ce69c9b..3f5b91c6 100644 --- a/unilabos-linux-64.yaml +++ b/unilabos-linux-64.yaml @@ -59,3 +59,5 @@ dependencies: # ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo # ilab equipments # - ros-humble-unilabos-msgs + - pip: + - paho-mqtt \ No newline at end of file diff --git a/unilabos-osx-64.yaml b/unilabos-osx-64.yaml index 7e21a65d..38981f0a 100644 --- a/unilabos-osx-64.yaml +++ b/unilabos-osx-64.yaml @@ -59,3 +59,5 @@ dependencies: # ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo # ilab equipments # - ros-humble-unilabos-msgs + - pip: + - paho-mqtt \ No newline at end of file diff --git a/unilabos-osx-arm64.yaml b/unilabos-osx-arm64.yaml index 4c69fb90..05333a39 100644 --- a/unilabos-osx-arm64.yaml +++ b/unilabos-osx-arm64.yaml @@ -61,3 +61,5 @@ dependencies: # ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo # ilab equipments # - ros-humble-unilabos-msgs + - pip: + - paho-mqtt \ No newline at end of file diff --git a/unilabos-win64.yaml b/unilabos-win64.yaml index 03f010dc..2e26fa39 100644 --- a/unilabos-win64.yaml +++ b/unilabos-win64.yaml @@ -58,4 +58,6 @@ dependencies: - ros-humble-simulation # ignored because of NO python3.11 package in WIN64 # ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo # ilab equipments -# - ros-humble-unilabos-msgs + # ros-humble-unilabos-msgs + - pip: + - paho-mqtt \ No newline at end of file diff --git a/unilabos/app/backend.py b/unilabos/app/backend.py index 8acc8251..5bc4ad39 100644 --- a/unilabos/app/backend.py +++ b/unilabos/app/backend.py @@ -7,7 +7,7 @@ from unilabos.utils import logger def start_backend( backend: str, devices_config: dict = {}, - resources_config: dict = {}, + resources_config: list = [], graph=None, controllers_config: dict = {}, bridges=[], diff --git a/unilabos/app/main.py b/unilabos/app/main.py index 16667d6c..4bc65cf5 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -1,30 +1,24 @@ import argparse import asyncio +import json import os import signal import sys -import json +import threading import time +from copy import deepcopy import yaml -from copy import deepcopy -import threading - -import rclpy -from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker # 首先添加项目根目录到路径 current_dir = os.path.dirname(os.path.abspath(__file__)) -ilabos_dir = os.path.dirname(os.path.dirname(current_dir)) -if ilabos_dir not in sys.path: - sys.path.append(ilabos_dir) +unilabos_dir = os.path.dirname(os.path.dirname(current_dir)) +if unilabos_dir not in sys.path: + sys.path.append(unilabos_dir) 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 -from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher -from unilabos.ros.nodes.presets.resource_mesh_manager import ResourceMeshManager -from rclpy.executors import MultiThreadedExecutor def parse_args(): @@ -83,9 +77,9 @@ def parse_args(): ) parser.add_argument( "--visual", - choices=["rviz", "web","None"], - default="rviz", - help="选择可视化工具: 'rviz' 或 'web' 或 'None',默认'rviz'", + choices=["rviz", "web", "deck", "disable"], + default="disable", + help="选择可视化工具: rviz, web, deck(2D bird view)", ) return parser.parse_args() @@ -137,7 +131,7 @@ def main(): # 注册表 build_registry(args_dict["registry_path"]) - + devices_and_resources = None if args_dict["graph"] is not None: import unilabos.resources.graphio as graph_res graph_res.physical_setup_graph = ( @@ -185,24 +179,22 @@ def main(): signal.signal(signal.SIGTERM, _exit) mqtt_client.start() args_dict["resources_mesh_config"] = {} - - if args_dict["visual"] != "None": - if args_dict["visual"] == "rviz": - enable_rviz=True - elif args_dict["visual"] == "web": - enable_rviz=False - resource_visualization = ResourceVisualization(devices_and_resources, args_dict["resources_config"] ,enable_rviz=enable_rviz) - - args_dict["resources_mesh_config"] = resource_visualization.resource_model - # 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中 - - start_backend(**args_dict) - server_thread = threading.Thread(target=start_server) - server_thread.start() - asyncio.set_event_loop(asyncio.new_event_loop()) - resource_visualization.start() - while True: - time.sleep(1) + # web visiualize 2D + if args_dict["visual"] != "disable": + enable_rviz = args_dict["visual"] == "rviz" + if devices_and_resources is not None: + 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) + server_thread.start() + asyncio.set_event_loop(asyncio.new_event_loop()) + resource_visualization.start() + while True: + time.sleep(1) + else: + start_backend(**args_dict) + start_server() else: start_backend(**args_dict) start_server() diff --git a/unilabos/app/web/client.py b/unilabos/app/web/client.py index 1957f5dd..da5d0696 100644 --- a/unilabos/app/web/client.py +++ b/unilabos/app/web/client.py @@ -9,6 +9,7 @@ from typing import List, Dict, Any, Optional import requests from unilabos.utils.log import info from unilabos.config.config import MQConfig, HTTPConfig +from unilabos.utils import logger class HTTPClient: @@ -102,6 +103,30 @@ class HTTPClient: ) return response + def upload_file(self, file_path: str, scene: str = "models") -> requests.Response: + """ + 上传文件到服务器 + + 使用multipart/form-data格式上传文件,类似curl -F "files=@filepath" + + Args: + file_path: 要上传的文件路径 + scene: 上传场景,可选值为"user"或"models",默认为"models" + + Returns: + Response: API响应对象 + """ + with open(file_path, "rb") as file: + files = {"files": file} + logger.info(f"上传文件: {file_path} 到 {scene}") + response = requests.post( + f"{self.remote_addr}/api/account/file_upload/{scene}", + files=files, + headers={"Authorization": f"lab {self.auth}"}, + timeout=30, # 上传文件可能需要更长的超时时间 + ) + return response + # 创建默认客户端实例 http_client = HTTPClient() diff --git a/unilabos/app/web/utils/action_utils.py b/unilabos/app/web/utils/action_utils.py index 1af458f5..be2baa3f 100644 --- a/unilabos/app/web/utils/action_utils.py +++ b/unilabos/app/web/utils/action_utils.py @@ -8,7 +8,7 @@ import traceback from typing import Dict, Any, Type, TypedDict, Optional from rclpy.action import ActionClient, ActionServer -from rosidl_parser.definition import UnboundedSequence, NamespacedType, BasicType +from rosidl_parser.definition import UnboundedSequence, NamespacedType, BasicType, UnboundedString from unilabos.ros.msgs.message_converter import msg_converter_manager from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode @@ -74,7 +74,6 @@ def get_yaml_from_goal_type(goal_type) -> str: for ind, slot_info in enumerate(goal_type._fields_and_field_types.items()): slot_name, slot_type = slot_info type_info = goal_type.SLOT_TYPES[ind] - default_value = "unknown" if isinstance(type_info, UnboundedSequence): inner_type = type_info.value_type if isinstance(inner_type, NamespacedType): @@ -83,8 +82,10 @@ def get_yaml_from_goal_type(goal_type) -> str: default_value = [get_ros_msg_instance_as_dict(type_class())] elif isinstance(inner_type, BasicType): default_value = [get_default_value_for_ros_type(inner_type.typename)] + elif isinstance(inner_type, UnboundedString): + default_value = [""] else: - default_value = "unknown" + default_value = [] elif isinstance(type_info, NamespacedType): cls_name = ".".join(type_info.namespaces) + ":" + type_info.name type_class = msg_converter_manager.get_class(cls_name) @@ -93,6 +94,8 @@ def get_yaml_from_goal_type(goal_type) -> str: default_value = get_ros_msg_instance_as_dict(type_class()) elif isinstance(type_info, BasicType): default_value = get_default_value_for_ros_type(type_info.typename) + elif isinstance(type_info, UnboundedString): + default_value = "" else: type_class = msg_converter_manager.search_class(slot_type, search_lower=True) if type_class is not None: diff --git a/unilabos/devices/laiyu_add_solid/laiyu.py b/unilabos/devices/laiyu_add_solid/laiyu.py new file mode 100644 index 00000000..0959f9a8 --- /dev/null +++ b/unilabos/devices/laiyu_add_solid/laiyu.py @@ -0,0 +1,304 @@ +import serial +import time +import pandas as pd + + +class Laiyu: + @property + def status(self) -> str: + return "" + + + def __init__(self, port, baudrate=115200, timeout=0.5): + """ + 初始化串口参数,默认波特率115200,8位数据位、1位停止位、无校验 + """ + self.ser = serial.Serial(port, baudrate=baudrate, timeout=timeout) + + def calculate_crc(self, data: bytes) -> bytes: + """ + 计算Modbus CRC-16,返回低字节和高字节(little-endian) + """ + crc = 0xFFFF + for pos in data: + crc ^= pos + for _ in range(8): + if crc & 0x0001: + crc = (crc >> 1) ^ 0xA001 + else: + crc >>= 1 + return crc.to_bytes(2, byteorder='little') + + def send_command(self, command: bytes) -> bytes: + """ + 构造完整指令帧(加上CRC校验),发送指令后一直等待设备响应,直至响应结束或超时(最大3分钟) + """ + crc = self.calculate_crc(command) + full_command = command + crc + # 清空接收缓存 + self.ser.reset_input_buffer() + self.ser.write(full_command) + print("发送指令:", full_command.hex().upper()) # 打印发送的指令帧 + + # 持续等待响应,直到连续0.5秒没有新数据或超时(3分钟) + start_time = time.time() + last_data_time = time.time() + response = bytearray() + while True: + if self.ser.in_waiting > 0: + new_data = self.ser.read(self.ser.in_waiting) + response.extend(new_data) + last_data_time = time.time() + # 如果已有数据,并且0.5秒内无新数据,则认为响应结束 + if response and (time.time() - last_data_time) > 0.5: + break + # 超过最大等待时间,退出循环 + if time.time() - start_time > 180: + break + time.sleep(0.1) + return bytes(response) + + def pick_powder_tube(self, int_input: int) -> bytes: + """ + 拿取粉筒指令: + - 功能码06 + - 寄存器地址0x0037(取粉筒) + - 数据:粉筒编号(如1代表A,2代表B,以此类推) + 示例:拿取A粉筒指令帧:01 06 00 37 00 01 + CRC + """ + slave_addr = 0x01 + function_code = 0x06 + register_addr = 0x0037 + # 数据部分:粉筒编号转换为2字节大端 + data = int_input.to_bytes(2, byteorder='big') + command = bytes([slave_addr, function_code]) + register_addr.to_bytes(2, byteorder='big') + data + return self.send_command(command) + + def put_powder_tube(self, int_input: int) -> bytes: + """ + 放回粉筒指令: + - 功能码06 + - 寄存器地址0x0038(放回粉筒) + - 数据:粉筒编号 + 示例:放回A粉筒指令帧:01 06 00 38 00 01 + CRC + """ + slave_addr = 0x01 + function_code = 0x06 + register_addr = 0x0038 + data = int_input.to_bytes(2, byteorder='big') + command = bytes([slave_addr, function_code]) + register_addr.to_bytes(2, byteorder='big') + data + return self.send_command(command) + + def reset(self) -> bytes: + """ + 重置指令: + - 功能码 0x06 + - 寄存器地址 0x0042 (示例中用了 00 42) + - 数据 0x0001 + 示例发送:01 06 00 42 00 01 E8 1E + """ + slave_addr = 0x01 + function_code = 0x06 + register_addr = 0x0042 # 对应示例中的 00 42 + payload = (0x0001).to_bytes(2, 'big') # 重置命令 + + cmd = ( + bytes([slave_addr, function_code]) + + register_addr.to_bytes(2, 'big') + + payload + ) + return self.send_command(cmd) + + + def move_to_xyz(self, x: float, y: float, z: float) -> bytes: + """ + 移动到指定位置指令: + - 功能码10(写多个寄存器) + - 寄存器起始地址0x0030 + - 寄存器数量:3个(x,y,z) + - 字节计数:6 + - 数据:x,y,z各2字节,单位为0.1mm(例如1mm对应数值10) + 示例帧:01 10 00 30 00 03 06 00C8 02BC 02EE + CRC + """ + slave_addr = 0x01 + function_code = 0x10 + register_addr = 0x0030 + num_registers = 3 + byte_count = num_registers * 2 # 6字节 + + # 将mm转换为0.1mm单位(乘以10),转换为2字节大端表示 + x_val = int(x * 10) + y_val = int(y * 10) + z_val = int(z * 10) + data = x_val.to_bytes(2, 'big') + y_val.to_bytes(2, 'big') + z_val.to_bytes(2, 'big') + + command = (bytes([slave_addr, function_code]) + + register_addr.to_bytes(2, 'big') + + num_registers.to_bytes(2, 'big') + + byte_count.to_bytes(1, 'big') + + data) + return self.send_command(command) + + def discharge(self, float_in: float) -> bytes: + """ + 出料指令: + - 使用写多个寄存器命令(功能码 0x10) + - 寄存器起始地址设为 0x0039 + - 寄存器数量为 0x0002(两个寄存器:出料质量和误差范围) + - 字节计数为 0x04(每个寄存器2字节,共4字节) + - 数据:出料质量(单位0.1mg,例如10mg对应100,即0x0064)、误差范围固定为0x0005 + 示例发送帧:01 10 00 39 0002 04 00640005 + CRC + """ + mass = float_in + slave_addr = 0x01 + function_code = 0x10 # 修改为写多个寄存器的功能码 + start_register = 0x0039 # 寄存器起始地址 + quantity = 0x0002 # 寄存器数量 + byte_count = 0x04 # 字节数:2寄存器*2字节=4 + mass_val = int(mass * 10) # 质量转换,单位0.1mg + error_margin = 5 # 固定误差范围,0x0005 + + command = (bytes([slave_addr, function_code]) + + start_register.to_bytes(2, 'big') + + quantity.to_bytes(2, 'big') + + byte_count.to_bytes(1, 'big') + + mass_val.to_bytes(2, 'big') + + error_margin.to_bytes(2, 'big')) + return self.send_command(command) + + + ''' + 示例:这个是标智96孔板的坐标转换,但是不同96孔板的坐标可能不同 + 所以需要根据实际情况进行修改 + ''' + + def move_to_plate(self, string): + #只接受两位数的str,比如a1,a2,b1,b2 + # 解析位置字符串 + if len(string) != 2 and len(string) != 3: + raise ValueError("Invalid plate position") + if not string[0].isalpha() or not string[1:].isdigit(): + raise ValueError("Invalid plate position") + a = string[0] # 字母部分s + b = string[1:] # 数字部分 + + if a.isalpha(): + a = ord(a.lower()) - ord('a') + 1 + else: + print('1') + raise ValueError("Invalid plate position") + a = int(a) + b = int(b) + # max a = 8, max b = 12, 否则报错 + if a > 8 or b > 12: + print('2') + raise ValueError("Invalid plate position") + # 计算移动到指定位置的坐标 + # a=1, x=3.0; a=12, x=220.0 + # b=1, y=62.0; b=8, y=201.0 + # z = 110.0 + x = float((b-1) * (220-4.0)/11 + 4.0) + y = float((a-1) * (201.0-62.0)/7 + 62.0) + z = 110.0 + # 移动到指定位置 + resp_move = self.move_to_xyz(x, y, z) + print("移动位置响应:", resp_move.hex().upper()) + # 打印移动到指定位置的坐标 + print(f"移动到位置:{string},坐标:x={x:.2f}, y={y:.2f}, z={z:.2f}") + return resp_move + + def add_powder_tube(self, powder_tube_number, target_tube_position, compound_mass): + # 拿取粉筒 + resp_pick = self.pick_powder_tube(powder_tube_number) + print("拿取粉筒响应:", resp_pick.hex().upper()) + time.sleep(1) + # 移动到指定位置 + self.move_to_plate(target_tube_position) + time.sleep(1) + # 出料,设定质量 + resp_discharge = self.discharge(compound_mass) + print("出料响应:", resp_discharge.hex().upper()) + # 使用modbus协议读取实际出料质量 + # 样例 01 06 00 40 00 64 89 F5,其中 00 64 是实际出料质量,换算为十进制为100,代表10 mg + # 从resp_discharge读取实际出料质量 + # 提取字节4和字节5的两个字节 + actual_mass_raw = int.from_bytes(resp_discharge[4:6], byteorder='big') + # 根据说明,将读取到的数据转换为实际出料质量(mg),这里除以10,例如:0x0064 = 100,转换后为10 mg + actual_mass_mg = actual_mass_raw / 10 + print(f"孔位{target_tube_position},实际出料质量:{actual_mass_mg}mg") + time.sleep(1) + # 放回粉筒 + resp_put = self.put_powder_tube(powder_tube_number) + print("放回粉筒响应:", resp_put.hex().upper()) + print(f"放回粉筒{powder_tube_number}") + resp_reset = self.reset() + return actual_mass_mg + + + +''' +样例:对单个粉筒进行称量 +''' + +modbus = Laiyu(port="COM25") + +mass_test = modbus.add_powder_tube(1, 'h12', 6.0) +print(f"实际出料质量:{mass_test}mg") + + +''' +样例: 对一份excel文件记录的化合物进行称量 +''' + +excel_file = r"C:\auto\laiyu\test1.xlsx" +# 定义输出文件路径,用于记录实际加样多少 +output_file = r"C:\auto\laiyu\test_output.xlsx" + +# 定义物料名称和料筒位置关系 +compound_positions = { + 'XPhos': '1', + 'Cu(OTf)2': '2', + 'CuSO4': '3', + 'PPh3': '4', +} + +# read excel file +# excel_file = r"C:\auto\laiyu\test.xlsx" +df = pd.read_excel(excel_file, sheet_name='Sheet1') +# 读取Excel文件中的数据 +# 遍历每一行数据 +for index, row in df.iterrows(): + # 获取物料名称和质量 + copper_name = row['copper'] + copper_mass = row['copper_mass'] + ligand_name = row['ligand'] + ligand_mass = row['ligand_mass'] + target_tube_position = row['position'] + # 获取物料位置 from compound_positions + copper_position = compound_positions.get(copper_name) + ligand_position = compound_positions.get(ligand_name) + # 判断物料位置是否存在 + if copper_position is None: + print(f"物料位置不存在:{copper_name}") + continue + if ligand_position is None: + print(f"物料位置不存在:{ligand_name}") + continue + # 加铜 + copper_actual_mass = modbus.add_powder_tube(int(copper_position), target_tube_position, copper_mass) + time.sleep(1) + # 加配体 + ligand_actual_mass = modbus.add_powder_tube(int(ligand_position), target_tube_position, ligand_mass) + time.sleep(1) + # 保存至df + df.at[index, 'copper_actual_mass'] = copper_actual_mass + df.at[index, 'ligand_actual_mass'] = ligand_actual_mass + +# 保存修改后的数据到新的Excel文件 +df.to_excel(output_file, index=False) +print(f"已保存到文件:{output_file}") + +# 关闭串口 +modbus.ser.close() +print("串口已关闭") + diff --git a/unilabos/devices/liquid_handling/action_definition.py b/unilabos/devices/liquid_handling/action_definition.py new file mode 100644 index 00000000..530703ad --- /dev/null +++ b/unilabos/devices/liquid_handling/action_definition.py @@ -0,0 +1,355 @@ +from __future__ import annotations + +from typing import List, Sequence, Optional, Literal, Union, Iterator + +import asyncio +import time + +from pylabrobot.liquid_handling import LiquidHandler +from pylabrobot.resources import ( + Resource, + TipRack, + Container, + Coordinate, + Well +) + +class DPLiquidHandler(LiquidHandler): + """Extended LiquidHandler with additional operations.""" + + # --------------------------------------------------------------- + # REMOVE LIQUID -------------------------------------------------- + # --------------------------------------------------------------- + + async def remove_liquid( + self, + vols: List[float], + sources: Sequence[Container], + waste_liquid: Optional[Container] = None, + *, + use_channels: Optional[List[int]] = None, + flow_rates: Optional[List[Optional[float]]] = None, + offsets: Optional[List[Coordinate]] = None, + liquid_height: Optional[List[Optional[float]]] = None, + blow_out_air_volume: Optional[List[Optional[float]]] = None, + 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] = [] + ): + """A complete *remove* (aspirate → waste) operation.""" + trash = self.deck.get_trash_area() + try: + if is_96_well: + 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) + 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 + 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, + blow_out_air_volume=blow_out_air_volume[0] if blow_out_air_volume else None, + spread=spread, + ) + 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 + + except Exception as e: + raise RuntimeError(f"Liquid removal failed: {e}") from e + + # --------------------------------------------------------------- + # ADD LIQUID ----------------------------------------------------- + # --------------------------------------------------------------- + + async def add_liquid( + self, + asp_vols: Union[List[float], float], + dis_vols: Union[List[float], float], + reagent_sources: Sequence[Container], + targets: Sequence[Container], + *, + use_channels: Optional[List[int]] = None, + flow_rates: Optional[List[Optional[float]]] = None, + offsets: Optional[List[Coordinate]] = None, + liquid_height: Optional[List[Optional[float]]] = None, + blow_out_air_volume: Optional[List[Optional[float]]] = None, + spread: Optional[Literal["wide", "tight", "custom"]] = "wide", + is_96_well: bool = False, + delays: Optional[List[int]] = None, + mix_time: Optional[int] = None, + mix_vol: Optional[int] = None, + mix_rate: Optional[int] = None, + mix_liquid_height: Optional[float] = None, + none_keys: List[str] = [] + ): + """A complete *add* (aspirate reagent → dispense into targets) operation.""" + + try: + if is_96_well: + pass # This mode is not verified. + else: + if len(asp_vols) != len(targets): + raise ValueError("Length of `vols` must match `targets`.") + tip = next(self.current_tip) + await self.pick_up_tips(tip) + + for _ in range(len(targets)): + await self.aspirate( + resources=reagent_sources, + vols=[asp_vols[_]], + use_channels=use_channels, + 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, + blow_out_air_volume=[blow_out_air_volume[0]] if blow_out_air_volume else None, + spread=spread + ) + if delays is not None: + await self.custom_delay(seconds=delays[0]) + await self.dispense( + resources=[targets[_]], + vols=[dis_vols[_]], + use_channels=use_channels, + flow_rates=[flow_rates[1]] if flow_rates else None, + offsets=[offsets[1]] if offsets else None, + blow_out_air_volume=[blow_out_air_volume[1]] if blow_out_air_volume else None, + liquid_height=[liquid_height[1]] if liquid_height else None, + spread=spread, + ) + if delays is not None: + await self.custom_delay(seconds=delays[1]) + await self.mix( + targets=targets[_], + mix_time=mix_time, + 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) + if delays is not None: + await self.custom_delay(seconds=delays[1]) + await self.touch_tip(targets[_]) + await self.discard_tips() + + except Exception as e: + raise RuntimeError(f"Liquid addition failed: {e}") from e + + # --------------------------------------------------------------- + # TRANSFER LIQUID ------------------------------------------------ + # --------------------------------------------------------------- + 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_flow_rates: Optional[List[Optional[float]]] = None, + dis_flow_rates: Optional[List[Optional[float]]] = None, + offsets: Optional[List[Coordinate]] = 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(int)] = 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] = [] + ): + """Transfer liquid from each *source* well/plate to the corresponding *target*. + + Parameters + ---------- + asp_vols, dis_vols + Single volume (µL) or list matching the number of transfers. + sources, targets + Same‑length sequences of containers (wells or plates). In 96‑well mode + each must contain exactly one plate. + tip_racks + One or more TipRacks providing fresh tips. + is_96_well + Set *True* to use the 96‑channel head. + """ + + try: + # ------------------------------------------------------------------ + # 96‑channel head mode + # ------------------------------------------------------------------ + if is_96_well: + 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)): + tip = next(tip_iter) + await self.pick_up_tips(tip) + # Aspirate from source + await self.aspirate( + resources=[src], + vols=[asp_vol], + use_channels=use_channels, + flow_rates=[asp_flow_rate], + offsets=offsets, + liquid_height=liquid_height, + blow_out_air_volume=blow_out_air_volume, + spread=spread, + ) + self.custom_delay(seconds=delays[0] if delays else 0) + # Dispense into target + await self.dispense( + resources=[tgt], + vols=[dis_vol], + use_channels=use_channels, + flow_rates=[dis_flow_rate], + offsets=offsets, + liquid_height=liquid_height, + blow_out_air_volume=blow_out_air_volume, + spread=spread, + ) + await self.mix( + targets=[tgt], + mix_time=mix_times[0] if mix_times else None, + mix_vol=mix_vol[0] if mix_vol else None, + mix_rate=mix_rate[0] if mix_rate else None, + ) + if touch_tip: + await self.touch_tip(tgt) + await self.discard_tips() + + except Exception as exc: + raise RuntimeError(f"Liquid transfer failed: {exc}") from exc + +# --------------------------------------------------------------- +# Helper utilities +# --------------------------------------------------------------- + + async def custom_delay(self, seconds=0, msg=None): + """ + seconds: seconds to wait + msg: information to be printed + """ + if seconds != None and seconds > 0: + if msg: + print(f"Waiting time: {msg}") + print(f"Current time: {time.strftime('%H:%M:%S')}") + print(f"Time to finish: {time.strftime('%H:%M:%S', time.localtime(time.time() + seconds))}") + await asyncio.sleep(seconds) + if msg: + print(f"Done: {msg}") + print(f"Current time: {time.strftime('%H:%M:%S')}") + + 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)], + liquid_height=None, + blow_out_air_volume=None + ) + #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)], + liquid_height=None, + blow_out_air_volume=None + ) + + async def mix( + self, + targets: Sequence[Container], + mix_time: int = None, + mix_vol: Optional[int] = None, + height_to_bottom: Optional[float] = None, + offsets: Optional[Coordinate] = None, + mix_rate: Optional[float] = None, + none_keys: List[str] = [] + ): + if mix_time is None: # No mixing required + return + """Mix the liquid in the target wells.""" + for _ in range(mix_time): + await self.aspirate( + resources=[targets], + vols=[mix_vol], + flow_rates=[mix_rate] if mix_rate else None, + offsets=[offsets] if offsets else None, + liquid_height=[height_to_bottom] if height_to_bottom else None, + ) + await self.custom_delay(seconds=1) + await self.dispense( + resources=[targets], + vols=[mix_vol], + flow_rates=[mix_rate] if mix_rate else None, + offsets=[offsets] if offsets else None, + liquid_height=[height_to_bottom] if height_to_bottom else None, + ) + + def iter_tips(self, tip_racks: Sequence[TipRack]) -> Iterator[Resource]: + """Yield tips from a list of TipRacks one-by-one until depleted.""" + for rack in tip_racks: + for tip in rack: + yield tip + raise RuntimeError("Out of tips!") + + def set_tiprack(self, tip_racks: Sequence[TipRack]): + """Set the tip racks for the liquid handler.""" + self.tip_racks = tip_racks + 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): + """ + Move a single channel to a specific well with a given z-height. + + Parameters + ---------- + well : Well + The target well. + dis_to_top : float + Height in mm to move to relative to the well top. + channel : int + Pipetting channel to move (default: 0). + """ + await self.prepare_for_manual_channel_operation(channel=channel) + abs_loc = well.get_absolute_location() + well_height = well.get_absolute_size_z() + 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) + diff --git a/unilabos/devices/liquid_handling/converted protocol/sci-lucif-assay4_plr_background_tested.ipynb b/unilabos/devices/liquid_handling/converted protocol/sci-lucif-assay4_plr_background_tested.ipynb new file mode 100644 index 00000000..daf47e23 --- /dev/null +++ b/unilabos/devices/liquid_handling/converted protocol/sci-lucif-assay4_plr_background_tested.ipynb @@ -0,0 +1,1291 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 9, + "id": "6e581f88", + "metadata": {}, + "outputs": [], + "source": [ + "# NF‑κB Luciferase Reporter Assay – pylabrobot version\n", + "\n", + "import os\n", + "import sys\n", + "os.getcwd()\n", + "sys.path.append('/Users/guangxinzhang/Documents/Deep Potential/pylabrobot/myfile')\n", + "\n", + "from pylabrobot.resources import Coordinate\n", + "from pylabrobot.liquid_handling.backends.chatterbox import LiquidHandlerChatterboxBackend\n", + "from pylabrobot.visualizer.visualizer import Visualizer\n", + "from pylabrobot.resources.opentrons import (\n", + " OTDeck,\n", + " corning_96_wellplate_360ul_flat,\n", + " nest_12_reservoir_15ml,\n", + " nest_1_reservoir_195ml,\n", + " opentrons_96_tiprack_300ul\n", + ")\n", + "from High_level_function.action_definition import DPLiquidHandler" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "c3127d6e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Setting up the liquid handler.\n", + "Resource deck was assigned to the liquid handler.\n", + "Resource trash_container was assigned to the liquid handler.\n", + "Resource tiprack_1 was assigned to the liquid handler.\n", + "Resource tiprack_4 was assigned to the liquid handler.\n", + "Resource tiprack_8 was assigned to the liquid handler.\n", + "Resource tiprack_11 was assigned to the liquid handler.\n", + "Resource working_plate was assigned to the liquid handler.\n", + "Resource reagent_stock was assigned to the liquid handler.\n", + "Resource waste_liq was assigned to the liquid handler.\n" + ] + } + ], + "source": [ + "# ──────────────────────────────────────\n", + "# User‑configurable constants (µL)\n", + "MEDIUM_VOL = 100 # volume of spent medium to remove\n", + "PBS_VOL = 50 # PBS wash volume\n", + "LYSIS_VOL = 30 # lysis buffer volume\n", + "LUC_VOL = 100 # luciferase reagent volume\n", + "TOTAL_COL = 12 # process A1–A12\n", + "\n", + "\n", + "# ──────────────────────────────────────\n", + "\n", + "lh = DPLiquidHandler(backend=LiquidHandlerChatterboxBackend(), deck=OTDeck())\n", + "await lh.setup()\n", + "#vis = Visualizer(resource=lh)\n", + "#await vis.setup()\n", + "\n", + "tiprack_slots = [1, 4, 8, 11]\n", + "tipracks = {\n", + " f\"tiprack_{slot}\": opentrons_96_tiprack_300ul(name=f\"tiprack_{slot}\")\n", + " for slot in tiprack_slots\n", + "}\n", + "\n", + "for name, tiprack in tipracks.items():\n", + " slot = int(name.split(\"_\")[1])\n", + " lh.deck.assign_child_at_slot(tiprack, slot=slot)\n", + "\n", + "# Working 96‑well plate at slot 6\n", + "working_plate = corning_96_wellplate_360ul_flat(name=\"working_plate\")\n", + "lh.deck.assign_child_at_slot(working_plate, slot=6)\n", + "\n", + "# 12‑channel reservoir (PBS, Lysis, Luciferase) at slot 3\n", + "reagent_stock = nest_12_reservoir_15ml(name='reagent_stock')\n", + "lh.deck.assign_child_at_slot(reagent_stock, slot=3)\n", + "\n", + "# 1‑channel waste reservoir at slot 9\n", + "waste_liq = nest_1_reservoir_195ml(name='waste_liq')\n", + "lh.deck.assign_child_at_slot(waste_liq, slot=9)" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "b5313453", + "metadata": {}, + "outputs": [], + "source": [ + "pbs = reagent_stock[0][0]\n", + "lysis = reagent_stock[1][0]\n", + "luciferase = reagent_stock[2][0]\n", + "waste_liq = waste_liq[0]\n", + "wells_name = [f\"A{i}\" for i in range(1, 13)]\n", + "cells_all = working_plate[wells_name] # A1–A12" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "e85d6752", + "metadata": {}, + "outputs": [], + "source": [ + "working_plate_volumes = [\n", + " ('culture medium', MEDIUM_VOL) if i % 8 == 0 else (None, 0)\n", + " for i in range(96)\n", + "]\n", + "working_plate.set_well_liquids(working_plate_volumes)\n", + "reagent_info = [('PBS Buffer', 5000), ('Lysis Buffer', 5000), ('Luciferase Reagent', 5000)]+[ (None, 0) ]* 9\n", + "reagent_stock.set_well_liquids(reagent_info)\n", + "lh.set_tiprack(list(tipracks.values()))" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "9dbfb0e2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(None, None)" + ] + }, + "execution_count": 13, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "from pylabrobot.resources import set_tip_tracking, set_volume_tracking\n", + "set_tip_tracking(True), set_volume_tracking(True)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "70094125", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_A1 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 working_plate_A1 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 waste_liq_A1 0,0,-5 3.0 None 0.0 \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_B1 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 working_plate_A2 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 waste_liq_A1 0,0,-5 3.0 None 0.0 \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_C1 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 working_plate_A3 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 waste_liq_A1 0,0,-5 3.0 None 0.0 \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_D1 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 working_plate_A4 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 waste_liq_A1 0,0,-5 3.0 None 0.0 \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_E1 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 working_plate_A5 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 waste_liq_A1 0,0,-5 3.0 None 0.0 \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_F1 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 working_plate_A6 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 waste_liq_A1 0,0,-5 3.0 None 0.0 \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_G1 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 working_plate_A7 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 waste_liq_A1 0,0,-5 3.0 None 0.0 \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_H1 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 working_plate_A8 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 waste_liq_A1 0,0,-5 3.0 None 0.0 \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_A2 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 working_plate_A9 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 waste_liq_A1 0,0,-5 3.0 None 0.0 \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_B2 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 working_plate_A10 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 waste_liq_A1 0,0,-5 3.0 None 0.0 \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_C2 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 working_plate_A11 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 waste_liq_A1 0,0,-5 3.0 None 0.0 \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_D2 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 working_plate_A12 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 waste_liq_A1 0,0,-5 3.0 None 0.0 \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n" + ] + } + ], + "source": [ + "await lh.remove_liquid(\n", + " vols=[MEDIUM_VOL]*12,\n", + " sources=cells_all,\n", + " waste_liquid=waste_liq,\n", + " top=[-0.2],\n", + " liquid_height=[0.2,0],\n", + " flow_rates=[0.2,3],\n", + " offsets=[Coordinate(-2.5, 0, 0),Coordinate(0, 0, -5)]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "91d07db6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_E2 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A1 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 waste_liq_A1 0,0,-5 3.0 None None \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_F2 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A2 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 waste_liq_A1 0,0,-5 3.0 None None \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_G2 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A3 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 waste_liq_A1 0,0,-5 3.0 None None \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_H2 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A4 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 waste_liq_A1 0,0,-5 3.0 None None \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_A3 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A5 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 waste_liq_A1 0,0,-5 3.0 None None \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_B3 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A6 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 waste_liq_A1 0,0,-5 3.0 None None \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_C3 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A7 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 waste_liq_A1 0,0,-5 3.0 None None \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_D3 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A8 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 waste_liq_A1 0,0,-5 3.0 None None \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_E3 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A9 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 waste_liq_A1 0,0,-5 3.0 None None \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_F3 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A10 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 waste_liq_A1 0,0,-5 3.0 None None \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_G3 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A11 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 waste_liq_A1 0,0,-5 3.0 None None \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n", + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_H3 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A12 -2.5,0,0 0.2 None 0.2 \n", + "[Well(name=waste_liq_A1, location=Coordinate(010.480, 007.140, 004.550), size_x=106.8, size_y=71.2, size_z=25, category=well)]\n", + "Tracker only has 0uL, please pay attention.\n", + "Container has too little liquid: 75.0uL > 0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 waste_liq_A1 0,0,-5 3.0 None None \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n" + ] + } + ], + "source": [ + "await lh.remove_liquid(\n", + " vols=[PBS_VOL*1.5]*12,\n", + " top=[-0.2],\n", + " liquid_height=[0.2,None],\n", + " offsets=[Coordinate(-2.5,0,0),Coordinate(0,0,-5)],\n", + " flow_rates=[0.2,3],\n", + " sources=cells_all,\n", + " waste_liquid=waste_liq\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "baa8f751", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_A4 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 reagent_stock_A2 0,0,0 0.5 None 0.5 \n", + "[Well(name=working_plate_A1, location=Coordinate(011.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 working_plate_A1 0,0,0 0.3 None 5.0 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A1 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A1 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 reagent_stock_A2 0,0,0 0.5 None 0.5 \n", + "[Well(name=working_plate_A2, location=Coordinate(020.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 working_plate_A2 0,0,0 0.3 None 5.0 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A2 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A2 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 reagent_stock_A2 0,0,0 0.5 None 0.5 \n", + "[Well(name=working_plate_A3, location=Coordinate(029.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 working_plate_A3 0,0,0 0.3 None 5.0 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A3 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A3 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 reagent_stock_A2 0,0,0 0.5 None 0.5 \n", + "[Well(name=working_plate_A4, location=Coordinate(038.955, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 working_plate_A4 0,0,0 0.3 None 5.0 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A4 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A4 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 reagent_stock_A2 0,0,0 0.5 None 0.5 \n", + "[Well(name=working_plate_A5, location=Coordinate(047.955, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 working_plate_A5 0,0,0 0.3 None 5.0 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A5 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A5 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 reagent_stock_A2 0,0,0 0.5 None 0.5 \n", + "[Well(name=working_plate_A6, location=Coordinate(056.955, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 working_plate_A6 0,0,0 0.3 None 5.0 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A6 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A6 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 reagent_stock_A2 0,0,0 0.5 None 0.5 \n", + "[Well(name=working_plate_A7, location=Coordinate(065.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 working_plate_A7 0,0,0 0.3 None 5.0 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A7 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A7 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 reagent_stock_A2 0,0,0 0.5 None 0.5 \n", + "[Well(name=working_plate_A8, location=Coordinate(074.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 working_plate_A8 0,0,0 0.3 None 5.0 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A8 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A8 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 reagent_stock_A2 0,0,0 0.5 None 0.5 \n", + "[Well(name=working_plate_A9, location=Coordinate(083.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 working_plate_A9 0,0,0 0.3 None 5.0 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A9 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A9 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 reagent_stock_A2 0,0,0 0.5 None 0.5 \n", + "[Well(name=working_plate_A10, location=Coordinate(092.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 working_plate_A10 0,0,0 0.3 None 5.0 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A10 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A10 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 reagent_stock_A2 0,0,0 0.5 None 0.5 \n", + "[Well(name=working_plate_A11, location=Coordinate(101.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 working_plate_A11 0,0,0 0.3 None 5.0 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A11 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A11 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 reagent_stock_A2 0,0,0 0.5 None 0.5 \n", + "[Well(name=working_plate_A12, location=Coordinate(110.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 30.0 working_plate_A12 0,0,0 0.3 None 5.0 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A12 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A12 2.4,0,0 None None None \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n" + ] + } + ], + "source": [ + "await lh.add_liquid(\n", + " asp_vols=[LYSIS_VOL]*12,\n", + " dis_vols=[LYSIS_VOL]*12,\n", + " reagent_sources=[lysis],\n", + " targets=cells_all,\n", + " flow_rates=[0.5,0.3],\n", + " liquid_height=[0.5,5],\n", + " delays=[2,2]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "d55b0875", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 17, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "lh.custom_delay(180)" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "20a281d3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Picking up tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: tiprack_1_B4 0,0,0 Tip 300.0 7.47 59.3 No \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 reagent_stock_A3 0,0,0 0.75 20.0 0.5 \n", + "[Well(name=working_plate_A1, location=Coordinate(011.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Tracker only has 100.0uL, please pay attention.\n", + "Container has too little liquid: 120.0uL > 100.0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 120.0 working_plate_A1 0,0,0 0.75 None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A1 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A1, location=Coordinate(011.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A1 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A1 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A1, location=Coordinate(011.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A1 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A1 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A1, location=Coordinate(011.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A1 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A1 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A1 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 reagent_stock_A3 0,0,0 0.75 20.0 0.5 \n", + "[Well(name=working_plate_A2, location=Coordinate(020.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Tracker only has 100.0uL, please pay attention.\n", + "Container has too little liquid: 120.0uL > 100.0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 120.0 working_plate_A2 0,0,0 0.75 None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A2 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A2, location=Coordinate(020.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A2 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A2 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A2, location=Coordinate(020.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A2 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A2 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A2, location=Coordinate(020.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A2 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A2 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A2 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 reagent_stock_A3 0,0,0 0.75 20.0 0.5 \n", + "[Well(name=working_plate_A3, location=Coordinate(029.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Tracker only has 100.0uL, please pay attention.\n", + "Container has too little liquid: 120.0uL > 100.0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 120.0 working_plate_A3 0,0,0 0.75 None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A3 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A3, location=Coordinate(029.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A3 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A3 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A3, location=Coordinate(029.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A3 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A3 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A3, location=Coordinate(029.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A3 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A3 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A3 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 reagent_stock_A3 0,0,0 0.75 20.0 0.5 \n", + "[Well(name=working_plate_A4, location=Coordinate(038.955, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Tracker only has 100.0uL, please pay attention.\n", + "Container has too little liquid: 120.0uL > 100.0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 120.0 working_plate_A4 0,0,0 0.75 None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A4 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A4, location=Coordinate(038.955, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A4 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A4 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A4, location=Coordinate(038.955, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A4 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A4 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A4, location=Coordinate(038.955, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A4 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A4 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A4 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 reagent_stock_A3 0,0,0 0.75 20.0 0.5 \n", + "[Well(name=working_plate_A5, location=Coordinate(047.955, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Tracker only has 100.0uL, please pay attention.\n", + "Container has too little liquid: 120.0uL > 100.0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 120.0 working_plate_A5 0,0,0 0.75 None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A5 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A5, location=Coordinate(047.955, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A5 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A5 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A5, location=Coordinate(047.955, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A5 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A5 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A5, location=Coordinate(047.955, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A5 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A5 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A5 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 reagent_stock_A3 0,0,0 0.75 20.0 0.5 \n", + "[Well(name=working_plate_A6, location=Coordinate(056.955, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Tracker only has 100.0uL, please pay attention.\n", + "Container has too little liquid: 120.0uL > 100.0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 120.0 working_plate_A6 0,0,0 0.75 None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A6 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A6, location=Coordinate(056.955, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A6 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A6 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A6, location=Coordinate(056.955, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A6 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A6 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A6, location=Coordinate(056.955, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A6 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A6 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A6 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 reagent_stock_A3 0,0,0 0.75 20.0 0.5 \n", + "[Well(name=working_plate_A7, location=Coordinate(065.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Tracker only has 100.0uL, please pay attention.\n", + "Container has too little liquid: 120.0uL > 100.0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 120.0 working_plate_A7 0,0,0 0.75 None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A7 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A7, location=Coordinate(065.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A7 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A7 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A7, location=Coordinate(065.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A7 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A7 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A7, location=Coordinate(065.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A7 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A7 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A7 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 reagent_stock_A3 0,0,0 0.75 20.0 0.5 \n", + "[Well(name=working_plate_A8, location=Coordinate(074.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Tracker only has 100.0uL, please pay attention.\n", + "Container has too little liquid: 120.0uL > 100.0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 120.0 working_plate_A8 0,0,0 0.75 None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A8 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A8, location=Coordinate(074.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A8 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A8 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A8, location=Coordinate(074.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A8 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A8 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A8, location=Coordinate(074.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A8 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A8 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A8 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 reagent_stock_A3 0,0,0 0.75 20.0 0.5 \n", + "[Well(name=working_plate_A9, location=Coordinate(083.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Tracker only has 100.0uL, please pay attention.\n", + "Container has too little liquid: 120.0uL > 100.0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 120.0 working_plate_A9 0,0,0 0.75 None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A9 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A9, location=Coordinate(083.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A9 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A9 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A9, location=Coordinate(083.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A9 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A9 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A9, location=Coordinate(083.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A9 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A9 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A9 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 reagent_stock_A3 0,0,0 0.75 20.0 0.5 \n", + "[Well(name=working_plate_A10, location=Coordinate(092.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Tracker only has 100.0uL, please pay attention.\n", + "Container has too little liquid: 120.0uL > 100.0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 120.0 working_plate_A10 0,0,0 0.75 None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A10 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A10, location=Coordinate(092.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A10 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A10 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A10, location=Coordinate(092.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A10 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A10 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A10, location=Coordinate(092.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A10 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A10 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A10 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 reagent_stock_A3 0,0,0 0.75 20.0 0.5 \n", + "[Well(name=working_plate_A11, location=Coordinate(101.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Tracker only has 100.0uL, please pay attention.\n", + "Container has too little liquid: 120.0uL > 100.0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 120.0 working_plate_A11 0,0,0 0.75 None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A11 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A11, location=Coordinate(101.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A11 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A11 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A11, location=Coordinate(101.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A11 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A11 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A11, location=Coordinate(101.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A11 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A11 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A11 2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 100.0 reagent_stock_A3 0,0,0 0.75 20.0 0.5 \n", + "[Well(name=working_plate_A12, location=Coordinate(110.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Tracker only has 100.0uL, please pay attention.\n", + "Container has too little liquid: 120.0uL > 100.0uL, please pay attention.\n", + "Air bubble detected, please pay attention.\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 120.0 working_plate_A12 0,0,0 0.75 None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A12 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A12, location=Coordinate(110.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A12 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A12 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A12, location=Coordinate(110.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A12 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A12 0,0,0 3.0 None 0.5 \n", + "[Well(name=working_plate_A12, location=Coordinate(110.954, 071.814, 003.550), size_x=4.851, size_y=4.851, size_z=10.67, category=well)]\n", + "Dispensing:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 75.0 working_plate_A12 0,0,0 3.0 None 0.5 \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A12 -2.4,0,0 None None None \n", + "Aspirating:\n", + "pip# vol(ul) resource offset flow rate blowout lld_z \n", + " p0: 0.0 working_plate_A12 2.4,0,0 None None None \n", + "Dropping tips:\n", + "pip# resource offset tip type max volume (µL) fitting depth (mm) tip length (mm) filter \n", + " p0: trash 0,0.0,0 Tip 300.0 7.47 59.3 No \n" + ] + } + ], + "source": [ + "await lh.add_liquid(\n", + " asp_vols=[LUC_VOL]*12,\n", + " dis_vols=[LUC_VOL+20]*12,\n", + " reagent_sources=[luciferase],\n", + " targets=cells_all,\n", + " liquid_height=[0.5, None],\n", + " mix_time=3,\n", + " mix_vol=75,\n", + " mix_rate=3,\n", + " mix_liquid_height=0.5,\n", + " delays = [2, None],\n", + " blow_out_air_volume=[20,None],\n", + " flow_rates=[0.75,0.75]\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "pylabrobot", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.17" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/unilabos/devices/zhida_hplc/possible_status.txt b/unilabos/devices/zhida_hplc/possible_status.txt new file mode 100644 index 00000000..cf4e9efb --- /dev/null +++ b/unilabos/devices/zhida_hplc/possible_status.txt @@ -0,0 +1,15 @@ +(base) PS C:\Users\dell\Desktop> python zhida.py getstatus +{ + "result": "RUN", + "message": "AcqTime:3.321049min Vial:1" +} +(base) PS C:\Users\dell\Desktop> python zhida.py getstatus +{ + "result": "NOTREADY", + "message": "AcqTime:0min Vial:1" +} +(base) PS C:\Users\dell\Desktop> python zhida.py getstatus +{ + "result": "PRERUN", + "message": "AcqTime:0min Vial:1" +} diff --git a/unilabos/devices/zhida_hplc/zhida.py b/unilabos/devices/zhida_hplc/zhida.py new file mode 100644 index 00000000..a6e1f9d3 --- /dev/null +++ b/unilabos/devices/zhida_hplc/zhida.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import socket +import json +import base64 +import argparse +import sys +import time + + +class ZhidaClient: + def __init__(self, host='192.168.1.47', port=5792, timeout=10.0): + self.host = host + self.port = port + self.timeout = timeout + self.sock = None + + def connect(self): + """建立 TCP 连接,并设置超时用于后续 recv/send。""" + self.sock = socket.create_connection((self.host, self.port), timeout=self.timeout) + # 确保后续 recv/send 都会在 timeout 秒后抛 socket.timeout + self.sock.settimeout(self.timeout) + + def close(self): + """关闭连接。""" + if self.sock: + self.sock.close() + self.sock = None + + def _send_command(self, cmd: dict) -> dict: + """ + 发送一条命令,接收 raw bytes,直到能成功 json.loads。 + """ + if not self.sock: + raise ConnectionError("Not connected") + + # 1) 发送 JSON 命令 + payload = json.dumps(cmd, ensure_ascii=False).encode('utf-8') + # 如果服务端需要换行分隔,也可以加上: payload += b'\n' + self.sock.sendall(payload) + + # 2) 循环 recv,直到能成功解析完整 JSON + buffer = bytearray() + start = time.time() + while True: + try: + chunk = self.sock.recv(4096) + if not chunk: + # 对端关闭 + break + buffer.extend(chunk) + # 尝试解码、解析 + text = buffer.decode('utf-8', errors='strict') + try: + return json.loads(text) + except json.JSONDecodeError: + # 继续 recv + pass + except socket.timeout: + raise TimeoutError("recv() timed out after {:.1f}s".format(self.timeout)) + # 可选:防止死循环,整个循环时长超过 2×timeout 就报错 + if time.time() - start > self.timeout * 2: + raise TimeoutError("No complete JSON received after {:.1f}s".format(time.time() - start)) + + raise ConnectionError("Connection closed before JSON could be parsed") + +# @property +# def xxx() -> 类型: +# return xxxxxx + +# def send_command(self, ): +# self.xxx = dict[xxx] + +# 示例响应回复: +# { +# "result": "RUN", +# "message": "AcqTime:3.321049min Vial:1" +# } + + @property + def status(self) -> dict: + return self._send_command({"command": "getstatus"})["result"] + + # def get_status(self) -> dict: + # print(self._send_command({"command": "getstatus"})) + # return self._send_command({"command": "getstatus"}) + + def get_methods(self) -> dict: + return self._send_command({"command": "getmethods"}) + + def start(self, text) -> dict: + b64 = base64.b64encode(text.encode('utf-8')).decode('ascii') + return self._send_command({"command": "start", "message": b64}) + + def abort(self) -> dict: + return self._send_command({"command": "abort"}) + +""" +a,b,c +1,2,4 +2,4,5 +""" + +client = ZhidaClient() +# 连接 +client.connect() +# 获取状态 +print(client.status) + + +# 命令格式:python zhida.py [options] diff --git a/unilabos/devices/zhida_hplc/zhida_test_1.csv b/unilabos/devices/zhida_hplc/zhida_test_1.csv new file mode 100644 index 00000000..96cef55e --- /dev/null +++ b/unilabos/devices/zhida_hplc/zhida_test_1.csv @@ -0,0 +1,2 @@ +SampleName,AcqMethod,RackCode,VialPos,SmplInjVol,OutputFile +Sample001,1028-10ul-10min.M,CStk1-01,1,10,DataSET1 \ No newline at end of file diff --git a/unilabos/registry/devices/laiyu_add_solid.yaml b/unilabos/registry/devices/laiyu_add_solid.yaml new file mode 100644 index 00000000..35540235 --- /dev/null +++ b/unilabos/registry/devices/laiyu_add_solid.yaml @@ -0,0 +1,56 @@ +laiyu_add_solid: + description: Laiyu Add Solid + class: + module: unilabos.devices.laiyu_add_solid.laiyu:Laiyu + type: python + status_types: {} + action_value_mappings: + move_to_xyz: + type: Point3DSeparateInput + goal: + x: x + y: y + z: z + feedback: {} + result: {} + pick_powder_tube: + type: IntSingleInput + goal: + int_input: int_input + feedback: {} + result: {} + put_powder_tube: + type: IntSingleInput + goal: + int_input: int_input + feedback: {} + result: {} + reset: + type: EmptyIn + goal: {} + feedback: {} + result: {} + add_powder_tube: + type: SolidDispenseAddPowderTube + goal: + powder_tube_number: powder_tube_number + target_tube_position: target_tube_position + compound_mass: compound_mass + feedback: {} + result: + actual_mass_mg: actual_mass_mg + move_to_plate: + type: StrSingleInput + goal: + string: string + feedback: {} + result: {} + discharge: + type: FloatSingleInput + goal: + float_input: float_input + feedback: {} + result: {} + + schema: + properties: {} \ No newline at end of file diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml index 4451ca06..ba921c77 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -163,13 +163,134 @@ liquid_handler: schema: type: object properties: - status: + name: type: string description: 液体处理仪器当前状态 required: - - status + - 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 @@ -179,7 +300,7 @@ liquid_handler.revvity: action_value_mappings: run: type: WorkStationRun - goal: + goal: wf_name: file_path params: params resource: resource @@ -187,4 +308,3 @@ liquid_handler.revvity: status: status result: success: success - \ No newline at end of file diff --git a/unilabos/registry/devices/robot_gripper.yaml b/unilabos/registry/devices/robot_gripper.yaml index 3cb86fed..bae970ac 100644 --- a/unilabos/registry/devices/robot_gripper.yaml +++ b/unilabos/registry/devices/robot_gripper.yaml @@ -19,9 +19,6 @@ gripper.mock: result: position: position effort: torque - model: - type: device - mesh: opentrons_liquid_handler gripper.misumi_rz: description: Misumi RZ gripper diff --git a/unilabos/registry/devices/zhida_hplc.yaml b/unilabos/registry/devices/zhida_hplc.yaml new file mode 100644 index 00000000..1aed8a27 --- /dev/null +++ b/unilabos/registry/devices/zhida_hplc.yaml @@ -0,0 +1,27 @@ +zhida_hplc: + description: Zhida HPLC + class: + module: unilabos.devices.zhida_hplc.zhida:ZhidaClient + type: python + status_types: + status: String + action_value_mappings: + start: + type: StrSingleInput + goal: + string: string + feedback: {} + result: {} + abort: + type: EmptyIn + goal: {} + feedback: {} + result: {} + get_methods: + type: EmptyIn + goal: {} + feedback: {} + result: {} + + schema: + properties: {} \ No newline at end of file diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py index feba3fef..d8459866 100644 --- a/unilabos/registry/registry.py +++ b/unilabos/registry/registry.py @@ -20,7 +20,43 @@ class Registry: self.registry_paths = DEFAULT_PATHS.copy() # 使用copy避免修改默认值 if registry_paths: self.registry_paths.extend(registry_paths) - self.device_type_registry = {} + action_type = self._replace_type_with_class( + "ResourceCreateFromOuter", "host_node", f"动作 add_resource_from_outer" + ) + schema = ros_action_to_json_schema(action_type) + self.device_type_registry = { + "host_node": { + "description": "UniLabOS主机节点", + "class": { + "module": "unilabos.ros.nodes.presets.host_node", + "type": "python", + "status_types": {}, + "action_value_mappings": { + "add_resource_from_outer": { + "type": msg_converter_manager.search_class("ResourceCreateFromOuter"), + "goal": { + "resources": "resources", + "device_ids": "device_ids", + "bind_parent_ids": "bind_parent_ids", + "bind_locations": "bind_locations", + "other_calling_params": "other_calling_params", + }, + "feedback": {}, + "result": { + "success": "success" + }, + "schema": schema + } + } + }, + "schema": { + "properties": {}, + "additionalProperties": False, + "type": "object" + }, + "file_path": "/" + } + } self.resource_type_registry = {} self._setup_called = False # 跟踪setup是否已调用 # 其他状态变量 diff --git a/unilabos/resources/registry.py b/unilabos/resources/registry.py index 435edb68..fae72622 100644 --- a/unilabos/resources/registry.py +++ b/unilabos/resources/registry.py @@ -1,5 +1,5 @@ -import sys - +import traceback +from unilabos.utils.log import logger resource_schema = { "workstation": {"type": "object", "properties": {}}, @@ -132,7 +132,8 @@ def add_schema(resources_config: list[dict]) -> list[dict]: try: if type(resource["children"][0]) == dict: resource["children"] = add_schema(resource["children"]) - except: - sys.exit(0) + except Exception as ex: + logger.error("添加物料schema时出错") + traceback.print_exc() return resources_config diff --git a/unilabos/ros/device_node_wrapper.py b/unilabos/ros/device_node_wrapper.py index 1697a9e8..f6d071f5 100644 --- a/unilabos/ros/device_node_wrapper.py +++ b/unilabos/ros/device_node_wrapper.py @@ -18,6 +18,7 @@ class ROS2DeviceNodeWrapper(ROS2DeviceNode): def ros2_device_node( cls: Type[T], + device_config: Optional[Dict[str, Any]] = None, status_types: Optional[Dict[str, Any]] = None, action_value_mappings: Optional[Dict[str, Any]] = None, hardware_interface: Optional[Dict[str, Any]] = None, @@ -30,6 +31,7 @@ def ros2_device_node( cls: 要封装的设备类 status_types: 需要发布的状态和传感器信息,每个(PROP: TYPE),PROP应该匹配cls.PROP或cls.get_PROP(), TYPE应该是ROS2消息类型。默认为{}。 + device_config: 初始化时的config。 action_value_mappings: 设备动作。默认为{}。 每个(ACTION: {'type': CMD_TYPE, 'goal': {FIELD: PROP}, 'feedback': {FIELD: PROP}, 'result': {FIELD: PROP}}), hardware_interface: 硬件接口配置。默认为{"name": "hardware_interface", "write": "send_command", "read": "read_data", "extra_info": []}。 @@ -42,6 +44,8 @@ def ros2_device_node( # 从属性中自动发现可发布状态 if status_types is None: status_types = {} + if device_config is None: + device_config = {} if action_value_mappings is None: action_value_mappings = {} if hardware_interface is None: @@ -73,6 +77,7 @@ def ros2_device_node( "__init__": lambda self, *args, **kwargs: init_wrapper( self, driver_class=cls, + device_config=device_config, status_types=status_types, action_value_mappings=action_value_mappings, hardware_interface=hardware_interface, diff --git a/unilabos/ros/initialize_device.py b/unilabos/ros/initialize_device.py index 730caa13..ed667f1e 100644 --- a/unilabos/ros/initialize_device.py +++ b/unilabos/ros/initialize_device.py @@ -1,9 +1,9 @@ -import rclpy -from rclpy.node import Node +import copy from typing import Optional + from unilabos.registry.registry import lab_registry -from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, DeviceInitError from unilabos.ros.device_node_wrapper import ros2_device_node +from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, DeviceInitError from unilabos.utils import logger from unilabos.utils.import_manager import default_manager @@ -22,17 +22,21 @@ def initialize_device_from_dict(device_id, device_config) -> Optional[ROS2Device None """ d = None + original_device_config = copy.deepcopy(device_config) device_class_config = device_config["class"] if isinstance(device_class_config, str): # 如果是字符串,则直接去lab_registry中查找,获取class if device_class_config not in lab_registry.device_type_registry: raise ValueError(f"Device class {device_class_config} not found.") device_class_config = device_config["class"] = lab_registry.device_type_registry[device_class_config]["class"] + else: + raise ValueError("不再支持class为字典传入,class必须为注册表中已经提供的设备,您可以新增注册表并通过--registry传入") if isinstance(device_class_config, dict): DEVICE = default_manager.get_class(device_class_config["module"]) # 不管是ros2的实例,还是python的,都必须包一次,除了HostNode DEVICE = ros2_device_node( DEVICE, status_types=device_class_config.get("status_types", {}), + device_config=original_device_config, action_value_mappings=device_class_config.get("action_value_mappings", {}), hardware_interface=device_class_config.get( "hardware_interface", diff --git a/unilabos/ros/main_slave_run.py b/unilabos/ros/main_slave_run.py index 4451831a..4ef43d63 100644 --- a/unilabos/ros/main_slave_run.py +++ b/unilabos/ros/main_slave_run.py @@ -44,18 +44,18 @@ def exit() -> None: def main( devices_config: Dict[str, Any] = {}, - resources_config={}, + resources_config: list=[], graph: Optional[Dict[str, Any]] = None, controllers_config: Dict[str, Any] = {}, bridges: List[Any] = [], - visual: str = "None", + visual: str = "disable", resources_mesh_config: dict = {}, - args: List[str] = ["--log-level", "debug"], + rclpy_init_args: List[str] = ["--log-level", "debug"], discovery_interval: float = 5.0, ) -> None: """主函数""" - rclpy.init(args=args) + rclpy.init(args=rclpy_init_args) executor = rclpy.__executor = MultiThreadedExecutor() # 创建主机节点 host_node = HostNode( @@ -68,21 +68,21 @@ def main( discovery_interval, ) - if visual != "None": + if visual != "disable": resource_mesh_manager = ResourceMeshManager( resources_mesh_config, resources_config, - resource_tracker= host_node.resource_tracker, + resource_tracker= DeviceNodeResourceTracker(), device_id = 'resource_mesh_manager', ) joint_republisher = JointRepublisher( 'joint_republisher', - host_node.resource_tracker + DeviceNodeResourceTracker() ) executor.add_node(resource_mesh_manager) executor.add_node(joint_republisher) - + thread = threading.Thread(target=executor.spin, daemon=True, name="host_executor_thread") thread.start() @@ -96,13 +96,13 @@ def slave( graph: Optional[Dict[str, Any]] = None, controllers_config: Dict[str, Any] = {}, bridges: List[Any] = [], - visual: str = "None", + visual: str = "disable", resources_mesh_config: dict = {}, - args: List[str] = ["--log-level", "debug"], + rclpy_init_args: List[str] = ["--log-level", "debug"], ) -> None: """从节点函数""" if not rclpy.ok(): - rclpy.init(args=args) + rclpy.init(args=rclpy_init_args) executor = rclpy.__executor if not executor: executor = rclpy.__executor = MultiThreadedExecutor() @@ -153,7 +153,7 @@ def slave( logger.info(f"Slave node info updated.") rclient = n.create_client(ResourceAdd, "/resources/add") - rclient.wait_for_service() # FIXME 可能一直等待,加一个参数 + rclient.wait_for_service() request = ResourceAdd.Request() request.resources = [convert_to_ros_msg(Resource, resource) for resource in resources_config] diff --git a/unilabos/ros/msgs/message_converter.py b/unilabos/ros/msgs/message_converter.py index 5e87fce5..01181c9f 100644 --- a/unilabos/ros/msgs/message_converter.py +++ b/unilabos/ros/msgs/message_converter.py @@ -566,6 +566,7 @@ basic_type_map = { 'float32': {'type': 'number'}, 'float64': {'type': 'number'}, 'string': {'type': 'string'}, + 'boolean': {'type': 'boolean'}, 'char': {'type': 'string', 'maxLength': 1}, 'byte': {'type': 'integer', 'minimum': 0, 'maximum': 255}, } diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index 7e032064..912c6745 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -15,8 +15,9 @@ from rclpy.action.server import ServerGoalHandle from rclpy.client import Client from rclpy.callback_groups import ReentrantCallbackGroup from rclpy.service import Service +from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response -from unilabos.resources.graphio import convert_resources_to_type, convert_resources_from_type +from unilabos.resources.graphio import convert_resources_to_type, convert_resources_from_type, resource_ulab_to_plr from unilabos.ros.msgs.message_converter import ( convert_to_ros_msg, convert_from_ros_msg, @@ -101,6 +102,7 @@ def init_wrapper( self, device_id: str, driver_class: type[T], + device_config: Dict[str, Any], status_types: Dict[str, Any], action_value_mappings: Dict[str, Any], hardware_interface: Dict[str, Any], @@ -118,6 +120,7 @@ def init_wrapper( children = [] kwargs["device_id"] = device_id kwargs["driver_class"] = driver_class + kwargs["device_config"] = device_config kwargs["driver_params"] = driver_params kwargs["status_types"] = status_types kwargs["action_value_mappings"] = action_value_mappings @@ -302,10 +305,52 @@ class BaseROS2DeviceNode(Node, Generic[T]): res.response = "" return res + def append_resource(req: SerialCommand_Request, res: SerialCommand_Response): + # 物料传输到对应的node节点 + rclient = self.create_client(ResourceAdd, "/resources/add") + rclient.wait_for_service() + request = ResourceAdd.Request() + command_json = json.loads(req.command) + namespace = command_json["namespace"] + bind_parent_id = command_json["bind_parent_id"] + edge_device_id = command_json["edge_device_id"] + location = command_json["bind_location"] + other_calling_param = command_json["other_calling_param"] + resources = command_json["resource"] + # 本地拿到这个物料,可能需要先做初始化? + if isinstance(resources, list): + request.resources = [convert_to_ros_msg(Resource, resource) for resource in resources] + else: + request.resources = [convert_to_ros_msg(Resource, resources)] + response = rclient.call(request) + # 应该本地先add_resource? + res.response = "OK" + # 接下来该根据bind_parent_id进行assign了,目前只有plr可以进行assign,不然没有办法输入到物料系统中 + resource = self.resource_tracker.figure_resource({"name": bind_parent_id}) + try: + from pylabrobot.resources.resource import Resource as ResourcePLR + from pylabrobot.resources.deck import Deck + from pylabrobot.resources import Coordinate + contain_model = not isinstance(resource, Deck) + if isinstance(resource, ResourcePLR): + # resources.list() + plr_instance = resource_ulab_to_plr(resources, contain_model) + resource.assign_child_resource(plr_instance, Coordinate(location["x"], location["y"], location["z"]), **other_calling_param) + except ImportError: + self.lab_logger().error("Host请求添加物料时,本环境并不存在pylabrobot") + except Exception as e: + self.lab_logger().error("Host请求添加物料时出错") + self.lab_logger().error(traceback.format_exc()) + return res + + # noinspection PyTypeChecker self._service_server: Dict[str, Service] = { "query_host_name": self.create_service( SerialCommand, f"/srv{self.namespace}/query_host_name", query_host_name_cb, callback_group=self.callback_group ), + "append_resource": self.create_service( + SerialCommand, f"/srv{self.namespace}/append_resource", append_resource, callback_group=self.callback_group + ), } # 向全局在线设备注册表添加设备信息 @@ -437,26 +482,26 @@ 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查询物料当前状态 - 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}") - try: - r = ResourceGet.Request() - r.id = action_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else action_kwargs[k][0]["id"] - r.with_children = True - response = await self._resource_clients["resource_get"].call_async(r) - except Exception: - logger.error(f"资源查询失败,默认使用本地资源") - # 删除对response.resources的检查,因为它总是存在 - resources_list = [convert_from_ros_msg(rs) for rs in response.resources] # type: ignore # FIXME - self.lab_logger().debug(f"资源查询结果: {len(resources_list)} 个资源") - type_hint = action_paramtypes[k] - final_type = get_type_class(type_hint) - # 判断 ACTION 是否需要特殊的物料类型如 pylabrobot.resources.Resource,并做转换 - final_resource = convert_resources_to_type(resources_list, final_type) - action_kwargs[k] = self.resource_tracker.figure_resource(final_resource) + # 向Host查询物料当前状态,如果是host本身的增加物料的请求,则直接跳过 + if action_name != "add_resource_from_outer": + 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}") + try: + r = ResourceGet.Request() + r.id = action_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else action_kwargs[k][0]["id"] + r.with_children = True + response = await self._resource_clients["resource_get"].call_async(r) + except Exception: + logger.error(f"资源查询失败,默认使用本地资源") + # 删除对response.resources的检查,因为它总是存在 + resources_list = [convert_from_ros_msg(rs) for rs in response.resources] # type: ignore # FIXME + self.lab_logger().debug(f"资源查询结果: {len(resources_list)} 个资源") + type_hint = action_paramtypes[k] + final_type = get_type_class(type_hint) + # 判断 ACTION 是否需要特殊的物料类型如 pylabrobot.resources.Resource,并做转换 + final_resource = convert_resources_to_type(resources_list, final_type) + action_kwargs[k] = self.resource_tracker.figure_resource(final_resource) self.lab_logger().info(f"准备执行: {action_kwargs}, 函数: {ACTION.__name__}") time_start = time.time() @@ -527,27 +572,28 @@ class BaseROS2DeviceNode(Node, Generic[T]): del future # 向Host更新物料当前状态 - for k, v in goal.get_fields_and_field_types().items(): - if v not in ["unilabos_msgs/Resource", "sequence"]: - continue - self.lab_logger().info(f"更新资源状态: {k}") - r = ResourceUpdate.Request() - # 仅当action_kwargs[k]不为None时尝试转换 - akv = action_kwargs[k] - apv = action_paramtypes[k] - final_type = get_type_class(apv) - if final_type is None: - continue - try: - r.resources = [ - convert_to_ros_msg(Resource, self.resource_tracker.root_resource(rs)) - for rs in convert_resources_from_type(akv, final_type) # type: ignore # FIXME # 考虑反查到最大的 - ] - response = await self._resource_clients["resource_update"].call_async(r) - self.lab_logger().debug(f"资源更新结果: {response}") - except Exception as e: - self.lab_logger().error(f"资源更新失败: {e}") - self.lab_logger().error(traceback.format_exc()) + if action_name != "add_resource_from_outer": + for k, v in goal.get_fields_and_field_types().items(): + if v not in ["unilabos_msgs/Resource", "sequence"]: + continue + self.lab_logger().info(f"更新资源状态: {k}") + r = ResourceUpdate.Request() + # 仅当action_kwargs[k]不为None时尝试转换 + akv = action_kwargs[k] + apv = action_paramtypes[k] + final_type = get_type_class(apv) + if final_type is None: + continue + try: + r.resources = [ + convert_to_ros_msg(Resource, self.resource_tracker.root_resource(rs)) + for rs in convert_resources_from_type(akv, final_type) # type: ignore # FIXME # 考虑反查到最大的 + ] + response = await self._resource_clients["resource_update"].call_async(r) + self.lab_logger().debug(f"资源更新结果: {response}") + except Exception as e: + self.lab_logger().error(f"资源更新失败: {e}") + self.lab_logger().error(traceback.format_exc()) # 发布结果 goal_handle.succeed() @@ -627,6 +673,7 @@ class ROS2DeviceNode: self, device_id: str, driver_class: Type[T], + device_config: Dict[str, Any], driver_params: Dict[str, Any], status_types: Dict[str, Any], action_value_mappings: Dict[str, Any], @@ -641,6 +688,8 @@ class ROS2DeviceNode: Args: device_id: 设备标识符 driver_class: 设备类 + device_config: 原始初始化的json + driver_params: driver初始化的参数 status_types: 状态类型映射 action_value_mappings: 动作值映射 hardware_interface: 硬件接口配置 @@ -657,11 +706,12 @@ class ROS2DeviceNode: # 保存设备类是否支持异步上下文 self._has_async_context = hasattr(driver_class, "__aenter__") and hasattr(driver_class, "__aexit__") self._driver_class = driver_class + self.device_config = device_config self.driver_is_ros = driver_is_ros self.resource_tracker = DeviceNodeResourceTracker() # use_pylabrobot_creator 使用 cls的包路径检测 - use_pylabrobot_creator = driver_class.__module__.startswith("pylabrobot") + use_pylabrobot_creator = driver_class.__module__.startswith("pylabrobot") or driver_class.__name__ == "DPLiquidHandler" # TODO: 要在创建之前预先请求服务器是否有当前id的物料,放到resource_tracker中,让pylabrobot进行创建 # 创建设备类实例 diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 5a739773..977ab31c 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -7,11 +7,13 @@ import uuid from typing import Optional, Dict, Any, List, ClassVar, Set from action_msgs.msg import GoalStatus -from unilabos_msgs.msg import Resource # type: ignore -from unilabos_msgs.srv import ResourceAdd, ResourceGet, ResourceDelete, ResourceUpdate, ResourceList, SerialCommand # type: ignore +from geometry_msgs.msg import Point from rclpy.action import ActionClient, get_action_server_names_and_types_by_node from rclpy.callback_groups import ReentrantCallbackGroup from rclpy.service import Service +from unilabos_msgs.msg import Resource # type: ignore +from unilabos_msgs.srv import ResourceAdd, ResourceGet, ResourceDelete, ResourceUpdate, ResourceList, \ + SerialCommand # type: ignore from unique_identifier_msgs.msg import UUID from unilabos.registry.registry import lab_registry @@ -23,11 +25,9 @@ from unilabos.ros.msgs.message_converter import ( convert_from_ros_msg, convert_to_ros_msg, msg_converter_manager, - ros_action_to_json_schema, ) from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode, DeviceNodeResourceTracker from unilabos.ros.nodes.presets.controller_node import ControllerNode -from unilabos.utils.type_check import TypeEncoder class HostNode(BaseROS2DeviceNode): @@ -50,7 +50,7 @@ class HostNode(BaseROS2DeviceNode): self, device_id: str, devices_config: Dict[str, Any], - resources_config: Any, + resources_config: list, physical_setup_graph: Optional[Dict[str, Any]] = None, controllers_config: Optional[Dict[str, Any]] = None, bridges: Optional[List[Any]] = None, @@ -76,7 +76,7 @@ class HostNode(BaseROS2DeviceNode): driver_instance=self, device_id=device_id, status_types={}, - action_value_mappings={}, + action_value_mappings=lab_registry.device_type_registry["host_node"]["class"]["action_value_mappings"], hardware_interface={}, print_publish=False, resource_tracker=DeviceNodeResourceTracker(), # host node并不是通过initialize 包一层传进来的 @@ -97,15 +97,13 @@ class HostNode(BaseROS2DeviceNode): self.bridges = bridges # 创建设备、动作客户端和目标存储 - self.devices_names: Dict[str, str] = {} # 存储设备名称和命名空间的映射 + 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_value_mappings: Dict[str, Dict] = ( - {} - ) # 用来存储多个ActionClient的type, goal, feedback, result的变量名映射关系 + self._action_value_mappings: Dict[str, Dict] = {} # 用来存储多个ActionClient的type, goal, feedback, result的变量名映射关系 self._goals: Dict[str, Any] = {} # 用来存储多个目标的状态 - self._online_devices: Set[str] = set() # 用于跟踪在线设备 + self._online_devices: Set[str] = {f"{self.namespace}/{device_id}"} # 用于跟踪在线设备 self._last_discovery_time = 0.0 # 上次设备发现的时间 self._discovery_lock = threading.Lock() # 设备发现的互斥锁 self._subscribed_topics = set() # 用于跟踪已订阅的话题 @@ -259,16 +257,41 @@ class HostNode(BaseROS2DeviceNode): self.lab_logger().debug(f"[Host Node] Created ActionClient (Discovery): {action_id}") action_name = action_id[len(namespace) + 1:] edge_device_id = namespace[9:] - from unilabos.app.mq import mqtt_client - info_with_schema = ros_action_to_json_schema(action_type) - mqtt_client.publish_actions(action_name, { - "device_id": edge_device_id, - "action_name": action_name, - "schema": info_with_schema, - }) + # from unilabos.app.mq import mqtt_client + # info_with_schema = ros_action_to_json_schema(action_type) + # mqtt_client.publish_actions(action_name, { + # "device_id": edge_device_id, + # "device_type": "", + # "action_name": action_name, + # "schema": info_with_schema, + # }) 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]): + 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 + srv_address = f"/srv{namespace}/append_resource" + sclient = self.create_client(SerialCommand, srv_address) + sclient.wait_for_service() + request = SerialCommand.Request() + request.command = json.dumps({ + "resource": resource, + "namespace": namespace, + "edge_device_id": device_id, + "bind_parent_id": bind_parent_id, + "bind_location": { + "x": bind_location.x, + "y": bind_location.y, + "z": bind_location.z, + }, + "other_calling_param": json.loads(other_calling_param) if other_calling_param else {}, + }, ensure_ascii=False) + response = sclient.call(request) + pass + pass + def initialize_device(self, device_id: str, device_config: Dict[str, Any]) -> None: """ 根据配置初始化设备, @@ -297,13 +320,14 @@ class HostNode(BaseROS2DeviceNode): action_type = action_value_mapping["type"] self._action_clients[action_id] = ActionClient(self, action_type, action_id) self.lab_logger().debug(f"[Host Node] Created ActionClient (Local): {action_id}") # 子设备再创建用的是Discover发现的 - from unilabos.app.mq import mqtt_client - info_with_schema = ros_action_to_json_schema(action_type) - mqtt_client.publish_actions(action_name, { - "device_id": device_id, - "action_name": action_name, - "schema": info_with_schema, - }) + # from unilabos.app.mq import mqtt_client + # info_with_schema = ros_action_to_json_schema(action_type) + # mqtt_client.publish_actions(action_name, { + # "device_id": device_id, + # "device_type": device_config["class"], + # "action_name": action_name, + # "schema": info_with_schema, + # }) else: self.lab_logger().warning(f"[Host Node] ActionClient {action_id} already exists.") device_key = f"{self.devices_names[device_id]}/{device_id}" # 这里不涉及二级device_id diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index dc9f9c4a..1115fcce 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -1,7 +1,7 @@ from unilabos.utils.log import logger -class DeviceNodeResourceTracker: +class DeviceNodeResourceTracker(object): def __init__(self): self.resources = [] @@ -15,44 +15,46 @@ class DeviceNodeResourceTracker: return resource def add_resource(self, resource): - # 使用内存地址跟踪是否为同一个resource for r in self.resources: if id(r) == id(resource): return - # 添加资源到跟踪器 self.resources.append(resource) def clear_resource(self): self.resources = [] - def figure_resource(self, resource): - # 使用内存地址跟踪是否为同一个resource - if isinstance(resource, list): - return [self.figure_resource(r) for r in resource] - res_id = resource.id if hasattr(resource, "id") else None - res_name = resource.name if hasattr(resource, "name") else None + def figure_resource(self, query_resource): + if isinstance(query_resource, list): + return [self.figure_resource(r) for r in query_resource] + res_id = query_resource.id if hasattr(query_resource, "id") else (query_resource.get("id") if isinstance(query_resource, dict) else None) + res_name = query_resource.name if hasattr(query_resource, "name") else (query_resource.get("name") if isinstance(query_resource, dict) else None) res_identifier = res_id if res_id else res_name identifier_key = "id" if res_id else "name" - resource_cls_type = type(resource) + resource_cls_type = type(query_resource) if res_identifier is None: - logger.warning(f"resource {resource} 没有id或name,暂不能对应figure") + logger.warning(f"resource {query_resource} 没有id或name,暂不能对应figure") res_list = [] for r in self.resources: - res_list.extend( - self.loop_find_resource(r, resource_cls_type, identifier_key, getattr(resource, identifier_key)) - ) + if isinstance(query_resource, dict): + res_list.extend( + self.loop_find_resource(r, resource_cls_type, identifier_key, query_resource[identifier_key]) + ) + else: + res_list.extend( + self.loop_find_resource(r, resource_cls_type, identifier_key, getattr(query_resource, identifier_key)) + ) assert len(res_list) == 1, f"找到多个资源,请检查资源是否唯一: {res_list}" - self.root_resource2resource[id(resource)] = res_list[0] + self.root_resource2resource[id(query_resource)] = res_list[0] # 后续加入其他对比方式 return res_list[0] - def loop_find_resource(self, resource, resource_cls_type, identifier_key, compare_value): + def loop_find_resource(self, resource, target_resource_cls_type, identifier_key, compare_value): res_list = [] - print(resource, resource_cls_type, identifier_key, compare_value) + # print(resource, target_resource_cls_type, identifier_key, compare_value) children = getattr(resource, "children", []) for child in children: - res_list.extend(self.loop_find_resource(child, resource_cls_type, identifier_key, compare_value)) - if resource_cls_type == type(resource): + res_list.extend(self.loop_find_resource(child, target_resource_cls_type, identifier_key, compare_value)) + if target_resource_cls_type == type(resource) or target_resource_cls_type == dict: if hasattr(resource, identifier_key): if getattr(resource, identifier_key) == compare_value: res_list.append(resource) diff --git a/unilabos/ros/utils/driver_creator.py b/unilabos/ros/utils/driver_creator.py index a63781ee..4e018e7e 100644 --- a/unilabos/ros/utils/driver_creator.py +++ b/unilabos/ros/utils/driver_creator.py @@ -219,12 +219,14 @@ class PyLabRobotCreator(DeviceClassCreator[T]): logger.error(f"PyLabRobot反序列化失败: {deserialize_error}") logger.error(f"PyLabRobot反序列化堆栈: {stack}") - return self.device_instance + return self.device_instance def post_create(self): if hasattr(self.device_instance, "setup") and asyncio.iscoroutinefunction(getattr(self.device_instance, "setup")): from unilabos.ros.nodes.base_device_node import ROS2DeviceNode - ROS2DeviceNode.run_async_func(getattr(self.device_instance, "setup")).add_done_callback(lambda x: logger.debug(f"PyLabRobot设备实例 {self.device_instance} 设置完成")) + def done_cb(*args): + logger.debug(f"PyLabRobot设备实例 {self.device_instance} 设置完成") + ROS2DeviceNode.run_async_func(getattr(self.device_instance, "setup")).add_done_callback(done_cb) class ProtocolNodeCreator(DeviceClassCreator[T]): diff --git a/unilabos_msgs/CMakeLists.txt b/unilabos_msgs/CMakeLists.txt index acaad771..69fbaa3a 100644 --- a/unilabos_msgs/CMakeLists.txt +++ b/unilabos_msgs/CMakeLists.txt @@ -43,6 +43,25 @@ 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/EmptyIn.action" + "action/FloatSingleInput.action" + "action/IntSingleInput.action" + "action/StrSingleInput.action" + "action/Point3DSeparateInput.action" + + "action/ResourceCreateFromOuter.action" + + "action/SolidDispenseAddPowderTube.action" + "action/PumpTransfer.action" "action/Clean.action" "action/Separate.action" diff --git a/unilabos_msgs/action/DPLiquidHandlerAddLiquid.action b/unilabos_msgs/action/DPLiquidHandlerAddLiquid.action new file mode 100644 index 00000000..0611b276 --- /dev/null +++ b/unilabos_msgs/action/DPLiquidHandlerAddLiquid.action @@ -0,0 +1,20 @@ +float64[] asp_vols +float64[] dis_vols +Resource[] reagent_sources +Resource[] targets +int32[] use_channels +float64[] flow_rates +geometry_msgs/Point[] offsets +float64[] liquid_height +float64[] blow_out_air_volume +string spread +bool is_96_well +int32 mix_time +int32 mix_vol +int32 mix_rate +float64 mix_liquid_height +string[] none_keys +--- +bool success +--- +# 反馈 \ No newline at end of file diff --git a/unilabos_msgs/action/DPLiquidHandlerCustomDelay.action b/unilabos_msgs/action/DPLiquidHandlerCustomDelay.action new file mode 100644 index 00000000..29f9b45b --- /dev/null +++ b/unilabos_msgs/action/DPLiquidHandlerCustomDelay.action @@ -0,0 +1,6 @@ +float64 seconds +string msg +--- +bool success +--- +# 反馈 \ No newline at end of file diff --git a/unilabos_msgs/action/DPLiquidHandlerMix.action b/unilabos_msgs/action/DPLiquidHandlerMix.action new file mode 100644 index 00000000..81d1b71c --- /dev/null +++ b/unilabos_msgs/action/DPLiquidHandlerMix.action @@ -0,0 +1,11 @@ +Resource[] targets +int32 mix_time +int32 mix_vol +float64 height_to_bottom +geometry_msgs/Point[] offsets +float64 mix_rate +string[] none_keys +--- +bool success +--- +# 反馈 \ No newline at end of file diff --git a/unilabos_msgs/action/DPLiquidHandlerMoveTo.action b/unilabos_msgs/action/DPLiquidHandlerMoveTo.action new file mode 100644 index 00000000..740d0fc6 --- /dev/null +++ b/unilabos_msgs/action/DPLiquidHandlerMoveTo.action @@ -0,0 +1,7 @@ +Resource well +float64 dis_to_top +int32 channel +--- +bool success +--- +# 反馈 \ No newline at end of file diff --git a/unilabos_msgs/action/DPLiquidHandlerRemoveLiquid.action b/unilabos_msgs/action/DPLiquidHandlerRemoveLiquid.action new file mode 100644 index 00000000..e6b43c53 --- /dev/null +++ b/unilabos_msgs/action/DPLiquidHandlerRemoveLiquid.action @@ -0,0 +1,17 @@ +float64[] vols +Resource[] sources +Resource waste_liquid +int32[] use_channels +float64[] flow_rates +geometry_msgs/Point[] offsets +float64[] liquid_height +float64[] blow_out_air_volume +string spread +int32[] delays +bool is_96_well +float64[] top +string[] none_keys +--- +bool success +--- +# 反馈 \ No newline at end of file diff --git a/unilabos_msgs/action/DPLiquidHandlerSetTiprack.action b/unilabos_msgs/action/DPLiquidHandlerSetTiprack.action new file mode 100644 index 00000000..437d3e3f --- /dev/null +++ b/unilabos_msgs/action/DPLiquidHandlerSetTiprack.action @@ -0,0 +1,5 @@ +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 new file mode 100644 index 00000000..e0c31046 --- /dev/null +++ b/unilabos_msgs/action/DPLiquidHandlerTouchTip.action @@ -0,0 +1,5 @@ +Resource[] targets +--- +bool success +--- +# 反馈 \ No newline at end of file diff --git a/unilabos_msgs/action/DPLiquidHandlerTransferLiquid.action b/unilabos_msgs/action/DPLiquidHandlerTransferLiquid.action new file mode 100644 index 00000000..39df59bb --- /dev/null +++ b/unilabos_msgs/action/DPLiquidHandlerTransferLiquid.action @@ -0,0 +1,25 @@ +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/EmptyIn.action b/unilabos_msgs/action/EmptyIn.action new file mode 100644 index 00000000..c44b70c0 --- /dev/null +++ b/unilabos_msgs/action/EmptyIn.action @@ -0,0 +1,4 @@ + +--- + +--- \ No newline at end of file diff --git a/unilabos_msgs/action/FloatSingleInput.action b/unilabos_msgs/action/FloatSingleInput.action new file mode 100644 index 00000000..2542d31f --- /dev/null +++ b/unilabos_msgs/action/FloatSingleInput.action @@ -0,0 +1,4 @@ +float64 float_in +--- +bool success +--- \ No newline at end of file diff --git a/unilabos_msgs/action/IntSingleInput.action b/unilabos_msgs/action/IntSingleInput.action new file mode 100644 index 00000000..0f8b7aaa --- /dev/null +++ b/unilabos_msgs/action/IntSingleInput.action @@ -0,0 +1,4 @@ +int32 int_input +--- +bool success +--- \ No newline at end of file diff --git a/unilabos_msgs/action/Point3DSeparateInput.action b/unilabos_msgs/action/Point3DSeparateInput.action new file mode 100644 index 00000000..4e15e8f8 --- /dev/null +++ b/unilabos_msgs/action/Point3DSeparateInput.action @@ -0,0 +1,6 @@ +float64 x +float64 y +float64 z +--- +bool success +--- \ No newline at end of file diff --git a/unilabos_msgs/action/ResourceCreateFromOuter.action b/unilabos_msgs/action/ResourceCreateFromOuter.action new file mode 100644 index 00000000..e0eeb1c7 --- /dev/null +++ b/unilabos_msgs/action/ResourceCreateFromOuter.action @@ -0,0 +1,8 @@ +Resource[] resources +string[] device_ids +string[] bind_parent_ids +geometry_msgs/Point[] bind_locations +string[] other_calling_params +--- +bool success +--- \ No newline at end of file diff --git a/unilabos_msgs/action/SolidDispenseAddPowderTube.action b/unilabos_msgs/action/SolidDispenseAddPowderTube.action new file mode 100644 index 00000000..674c4ffc --- /dev/null +++ b/unilabos_msgs/action/SolidDispenseAddPowderTube.action @@ -0,0 +1,7 @@ +int32 powder_tube_number +string target_tube_position +float64 compound_mass +--- +float64 actual_mass_mg +bool success +--- \ No newline at end of file diff --git a/unilabos_msgs/action/StrSingleInput.action b/unilabos_msgs/action/StrSingleInput.action new file mode 100644 index 00000000..bb762a58 --- /dev/null +++ b/unilabos_msgs/action/StrSingleInput.action @@ -0,0 +1,4 @@ +string string +--- +bool success +--- \ No newline at end of file