From e8d12634884d528914684472135c19d5450b96a0 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 3 Feb 2026 18:10:32 +0800 Subject: [PATCH] workflow upload & prcxi transfer liquid --- .../liquid_handler_abstract.py | 47 +++++--- .../devices/liquid_handling/prcxi/prcxi.py | 25 ++-- unilabos/registry/devices/liquid_handler.yaml | 114 +++++++++++++----- unilabos/ros/nodes/base_device_node.py | 6 +- unilabos/ros/nodes/presets/host_node.py | 2 +- 5 files changed, 133 insertions(+), 61 deletions(-) diff --git a/unilabos/devices/liquid_handling/liquid_handler_abstract.py b/unilabos/devices/liquid_handling/liquid_handler_abstract.py index e084551..35aba21 100644 --- a/unilabos/devices/liquid_handling/liquid_handler_abstract.py +++ b/unilabos/devices/liquid_handling/liquid_handler_abstract.py @@ -1,15 +1,11 @@ from __future__ import annotations -import asyncio import time import traceback from collections import Counter from typing import List, Sequence, Optional, Literal, Union, Iterator, Dict, Any, Callable, Set, cast -from typing_extensions import TypedDict from pylabrobot.liquid_handling import LiquidHandler, LiquidHandlerBackend, LiquidHandlerChatterboxBackend, Strictness -from unilabos.devices.liquid_handling.rviz_backend import UniLiquidHandlerRvizBackend -from unilabos.devices.liquid_handling.laiyu.backend.laiyu_v_backend import UniLiquidHandlerLaiyuBackend from pylabrobot.liquid_handling.liquid_handler import TipPresenceProbingMethod from pylabrobot.liquid_handling.standard import GripDirection from pylabrobot.resources import ( @@ -27,26 +23,33 @@ from pylabrobot.resources import ( Trash, Tip, ) +from typing_extensions import TypedDict +from unilabos.devices.liquid_handling.rviz_backend import UniLiquidHandlerRvizBackend from unilabos.registry.placeholder_type import ResourceSlot -from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode -from unilabos.resources.resource_tracker import ResourceTreeSet +from unilabos.resources.resource_tracker import ResourceTreeSet, ResourceDict +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode class SimpleReturn(TypedDict): - samples: list - volumes: list + samples: List[List[ResourceDict]] + volumes: List[float] class SetLiquidReturn(TypedDict): - wells: list - volumes: list + wells: List[List[ResourceDict]] + volumes: List[float] class SetLiquidFromPlateReturn(TypedDict): - plate: list - wells: list - volumes: list + plate: List[List[ResourceDict]] + wells: List[List[ResourceDict]] + volumes: List[float] + + +class TransferLiquidReturn(TypedDict): + sources: List[List[ResourceDict]] + targets: List[List[ResourceDict]] class LiquidHandlerMiddleware(LiquidHandler): @@ -682,9 +685,8 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware): wells=ResourceTreeSet.from_plr_resources(wells, known_newly_created=False).dump(), volumes=res_volumes # type: ignore ) - @classmethod def set_liquid_from_plate( - cls, plate: List[ResourceSlot], well_names: list[str], liquid_names: list[str], volumes: list[float] + self, plate: List[ResourceSlot], well_names: list[str], liquid_names: list[str], volumes: list[float] ) -> SetLiquidFromPlateReturn: """Set the liquid in wells of a plate by well names (e.g., A1, A2, B3). @@ -710,6 +712,14 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware): well.set_liquids([(liquid_name, volume)]) # type: ignore res_volumes.append(volume) + task = ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{"resources": wells}) + submit_time = time.time() + while not task.done(): + if time.time() - submit_time > 10: + self._ros_node.lab_logger().info(f"set_liquid_from_plate {plate} 超时") + break + time.sleep(0.01) + return SetLiquidFromPlateReturn( plate=ResourceTreeSet.from_plr_resources([plate], known_newly_created=False).dump(), # type: ignore wells=ResourceTreeSet.from_plr_resources(wells, known_newly_created=False).dump(), # type: ignore @@ -1115,7 +1125,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware): mix_liquid_height: Optional[float] = None, delays: Optional[List[int]] = None, none_keys: List[str] = [], - ): + ) -> TransferLiquidReturn: """Transfer liquid with automatic mode detection. Supports three transfer modes: @@ -1255,6 +1265,11 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware): "Supported modes: 1->N, N->1, or N->N." ) + return TransferLiquidReturn( + sources=ResourceTreeSet.from_plr_resources(list(sources), known_newly_created=False).dump(), # type: ignore + targets=ResourceTreeSet.from_plr_resources(list(targets), known_newly_created=False).dump(), # type: ignore + ) + async def _transfer_one_to_one( self, sources: Sequence[Container], diff --git a/unilabos/devices/liquid_handling/prcxi/prcxi.py b/unilabos/devices/liquid_handling/prcxi/prcxi.py index 453f506..f337529 100644 --- a/unilabos/devices/liquid_handling/prcxi/prcxi.py +++ b/unilabos/devices/liquid_handling/prcxi/prcxi.py @@ -52,6 +52,7 @@ from unilabos.devices.liquid_handling.liquid_handler_abstract import ( SimpleReturn, SetLiquidReturn, SetLiquidFromPlateReturn, + TransferLiquidReturn, ) from unilabos.registry.placeholder_type import ResourceSlot from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode @@ -154,25 +155,29 @@ class PRCXI9300Plate(Plate): **kwargs, ): # 如果 ordered_items 不为 None,直接使用 + items = None + ordering_param = None if ordered_items is not None: items = ordered_items elif ordering is not None: # 检查 ordering 中的值是否是字符串(从 JSON 反序列化时的情况) # 如果是字符串,说明这是位置名称,需要让 Plate 自己创建 Well 对象 # 我们只传递位置信息(键),不传递值,使用 ordering 参数 - if ordering and isinstance(next(iter(ordering.values()), None), str): - # ordering 的值是字符串,只使用键(位置信息)创建新的 OrderedDict - # 传递 ordering 参数而不是 ordered_items,让 Plate 自己创建 Well 对象 - items = None - # 使用 ordering 参数,只包含位置信息(键) - ordering_param = collections.OrderedDict((k, None) for k in ordering.keys()) + if ordering: + values = list(ordering.values()) + value = values[0] + if isinstance(value, str): + # ordering 的值是字符串,只使用键(位置信息)创建新的 OrderedDict + # 传递 ordering 参数而不是 ordered_items,让 Plate 自己创建 Well 对象 + items = None + # 使用 ordering 参数,只包含位置信息(键) + ordering_param = collections.OrderedDict((k, None) for k in ordering.keys()) + elif value is None: + ordering_param = ordering else: # ordering 的值已经是对象,可以直接使用 items = ordering ordering_param = None - else: - items = None - ordering_param = None # 根据情况传递不同的参数 if items is not None: @@ -713,7 +718,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract): mix_liquid_height: Optional[float] = None, delays: Optional[List[int]] = None, none_keys: List[str] = [], - ): + ) -> TransferLiquidReturn: return await super().transfer_liquid( sources, targets, diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml index 1cd48f2..319d033 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -638,7 +638,7 @@ liquid_handler: placeholder_keys: {} result: {} schema: - description: 吸头迭代函数。用于自动管理和切换吸头架中的吸头,实现批量实验中的吸头自动分配和追踪。该函数监控吸头使用状态,自动切换到下一个可用吸头位置,确保实验流程的连续性。适用于高通量实验、批量处理、自动化流水线等需要大量吸头管理的应用场景。 + description: 吸头迭代函数。用于自动管理和切换枪头盒中的吸头,实现批量实验中的吸头自动分配和追踪。该函数监控吸头使用状态,自动切换到下一个可用吸头位置,确保实验流程的连续性。适用于高通量实验、批量处理、自动化流水线等需要大量吸头管理的应用场景。 properties: feedback: {} goal: @@ -712,6 +712,43 @@ liquid_handler: title: set_group参数 type: object type: UniLabJsonCommand + auto-set_liquid_from_plate: + feedback: {} + goal: {} + goal_default: + liquid_names: null + plate: null + volumes: null + well_names: null + handles: {} + placeholder_keys: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + liquid_names: + type: string + plate: + type: string + volumes: + type: string + well_names: + type: string + required: + - plate + - well_names + - liquid_names + - volumes + type: object + result: {} + required: + - goal + title: set_liquid_from_plate参数 + type: object + type: UniLabJsonCommand auto-set_tiprack: feedback: {} goal: {} @@ -721,7 +758,7 @@ liquid_handler: placeholder_keys: {} result: {} schema: - description: 吸头架设置函数。用于配置和初始化液体处理系统的吸头架信息,包括吸头架位置、类型、容量等参数。该函数建立吸头资源管理系统,为后续的吸头选择和使用提供基础配置。适用于系统初始化、吸头架更换、实验配置等需要吸头资源管理的操作场景。 + description: 枪头盒设置函数。用于配置和初始化液体处理系统的枪头盒信息,包括枪头盒位置、类型、容量等参数。该函数建立吸头资源管理系统,为后续的吸头选择和使用提供基础配置。适用于系统初始化、枪头盒更换、实验配置等需要吸头资源管理的操作场景。 properties: feedback: {} goal: @@ -4093,32 +4130,32 @@ liquid_handler: - 0 handles: input: - - data_key: liquid + - data_key: sources data_source: handle data_type: resource handler_key: sources - label: sources - - data_key: liquid - data_source: executor - data_type: resource - handler_key: targets - label: targets - - data_key: liquid - data_source: executor - data_type: resource - handler_key: tip_rack - label: tip_rack - output: - - data_key: liquid + label: 待移动液体 + - data_key: targets data_source: handle data_type: resource + handler_key: targets + label: 转移目标 + - data_key: tip_racks + data_source: handle + data_type: resource + handler_key: tip_rack + label: 枪头盒 + output: + - data_key: sources.@flatten + data_source: executor + data_type: resource handler_key: sources_out - label: sources - - data_key: liquid + label: 移液后源孔 + - data_key: targets.@flatten data_source: executor data_type: resource handler_key: targets_out - label: targets + label: 移液后目标孔 placeholder_keys: sources: unilabos_resources targets: unilabos_resources @@ -5114,19 +5151,34 @@ liquid_handler.biomek: - 0 handles: input: - - data_key: liquid + - data_key: sources data_source: handle data_type: resource - handler_key: liquid-input + handler_key: sources io_type: target - label: Liquid Input + label: 待移动液体 + - data_key: targets + data_source: handle + data_type: resource + handler_key: targets + label: 转移目标 + - data_key: tip_racks + data_source: handle + data_type: resource + handler_key: tip_rack + label: 枪头盒 output: - - data_key: liquid + - data_key: sources.@flatten data_source: executor data_type: resource - handler_key: liquid-output + handler_key: sources_out io_type: source - label: Liquid Output + label: 移液后源孔 + - data_key: targets.@flatten + data_source: executor + data_type: resource + handler_key: targets_out + label: 移液后目标孔 placeholder_keys: sources: unilabos_resources targets: unilabos_resources @@ -9924,18 +9976,18 @@ liquid_handler.prcxi: data_source: handle data_type: resource handler_key: tip_rack_identifier - label: 墙头盒 + label: 枪头盒 output: - - data_key: liquid - data_source: handle + - data_key: sources.@flatten + data_source: executor data_type: resource handler_key: sources_out - label: sources - - data_key: liquid + label: 移液后源孔 + - data_key: targets.@flatten data_source: executor data_type: resource handler_key: targets_out - label: targets + label: 移液后目标孔 placeholder_keys: sources: unilabos_resources targets: unilabos_resources diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index 3d1ffda..56585f6 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -1581,7 +1581,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): f"转换ResourceSlot列表参数 {arg_name} 失败: {e}\n{traceback.format_exc()}" ) raise JsonCommandInitError(f"ResourceSlot列表参数转换失败: {arg_name}") - + # todo: 默认反报送 return function(**function_args) except KeyError as ex: raise JsonCommandInitError( @@ -1614,8 +1614,8 @@ class BaseROS2DeviceNode(Node, Generic[T]): timeout = 30.0 elapsed = 0.0 while not future.done() and elapsed < timeout: - time.sleep(0.05) - elapsed += 0.05 + time.sleep(0.02) + elapsed += 0.02 if not future.done(): raise Exception(f"资源查询超时: {uuids_list}") diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 6d5e2b4..4cfa9c1 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -807,7 +807,7 @@ class HostNode(BaseROS2DeviceNode): assign_sample_id(action_kwargs) goal_msg = convert_to_ros_msg(action_client._action_type.Goal(), action_kwargs) - self.lab_logger().info(f"[Host Node] Sending goal for {action_id}: {str(goal_msg)[:1000]}") + # self.lab_logger().trace(f"[Host Node] Sending goal for {action_id}: {str(goal_msg)[:1000]}") self.lab_logger().trace(f"[Host Node] Sending goal for {action_id}: {action_kwargs}") self.lab_logger().trace(f"[Host Node] Sending goal for {action_id}: {goal_msg}") action_client.wait_for_server()