workflow upload & prcxi transfer liquid

This commit is contained in:
Xuwznln
2026-02-03 18:10:32 +08:00
parent 380b39100d
commit e8d1263488
5 changed files with 133 additions and 61 deletions

View File

@@ -1,15 +1,11 @@
from __future__ import annotations from __future__ import annotations
import asyncio
import time import time
import traceback import traceback
from collections import Counter from collections import Counter
from typing import List, Sequence, Optional, Literal, Union, Iterator, Dict, Any, Callable, Set, cast 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 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.liquid_handler import TipPresenceProbingMethod
from pylabrobot.liquid_handling.standard import GripDirection from pylabrobot.liquid_handling.standard import GripDirection
from pylabrobot.resources import ( from pylabrobot.resources import (
@@ -27,26 +23,33 @@ from pylabrobot.resources import (
Trash, Trash,
Tip, Tip,
) )
from typing_extensions import TypedDict
from unilabos.devices.liquid_handling.rviz_backend import UniLiquidHandlerRvizBackend
from unilabos.registry.placeholder_type import ResourceSlot from unilabos.registry.placeholder_type import ResourceSlot
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode from unilabos.resources.resource_tracker import ResourceTreeSet, ResourceDict
from unilabos.resources.resource_tracker import ResourceTreeSet from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode
class SimpleReturn(TypedDict): class SimpleReturn(TypedDict):
samples: list samples: List[List[ResourceDict]]
volumes: list volumes: List[float]
class SetLiquidReturn(TypedDict): class SetLiquidReturn(TypedDict):
wells: list wells: List[List[ResourceDict]]
volumes: list volumes: List[float]
class SetLiquidFromPlateReturn(TypedDict): class SetLiquidFromPlateReturn(TypedDict):
plate: list plate: List[List[ResourceDict]]
wells: list wells: List[List[ResourceDict]]
volumes: list volumes: List[float]
class TransferLiquidReturn(TypedDict):
sources: List[List[ResourceDict]]
targets: List[List[ResourceDict]]
class LiquidHandlerMiddleware(LiquidHandler): 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 wells=ResourceTreeSet.from_plr_resources(wells, known_newly_created=False).dump(), volumes=res_volumes # type: ignore
) )
@classmethod
def set_liquid_from_plate( 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: ) -> SetLiquidFromPlateReturn:
"""Set the liquid in wells of a plate by well names (e.g., A1, A2, B3). """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 well.set_liquids([(liquid_name, volume)]) # type: ignore
res_volumes.append(volume) 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( return SetLiquidFromPlateReturn(
plate=ResourceTreeSet.from_plr_resources([plate], known_newly_created=False).dump(), # type: ignore 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 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, mix_liquid_height: Optional[float] = None,
delays: Optional[List[int]] = None, delays: Optional[List[int]] = None,
none_keys: List[str] = [], none_keys: List[str] = [],
): ) -> TransferLiquidReturn:
"""Transfer liquid with automatic mode detection. """Transfer liquid with automatic mode detection.
Supports three transfer modes: Supports three transfer modes:
@@ -1255,6 +1265,11 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware):
"Supported modes: 1->N, N->1, or N->N." "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( async def _transfer_one_to_one(
self, self,
sources: Sequence[Container], sources: Sequence[Container],

View File

@@ -52,6 +52,7 @@ from unilabos.devices.liquid_handling.liquid_handler_abstract import (
SimpleReturn, SimpleReturn,
SetLiquidReturn, SetLiquidReturn,
SetLiquidFromPlateReturn, SetLiquidFromPlateReturn,
TransferLiquidReturn,
) )
from unilabos.registry.placeholder_type import ResourceSlot from unilabos.registry.placeholder_type import ResourceSlot
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
@@ -154,25 +155,29 @@ class PRCXI9300Plate(Plate):
**kwargs, **kwargs,
): ):
# 如果 ordered_items 不为 None直接使用 # 如果 ordered_items 不为 None直接使用
items = None
ordering_param = None
if ordered_items is not None: if ordered_items is not None:
items = ordered_items items = ordered_items
elif ordering is not None: elif ordering is not None:
# 检查 ordering 中的值是否是字符串(从 JSON 反序列化时的情况) # 检查 ordering 中的值是否是字符串(从 JSON 反序列化时的情况)
# 如果是字符串,说明这是位置名称,需要让 Plate 自己创建 Well 对象 # 如果是字符串,说明这是位置名称,需要让 Plate 自己创建 Well 对象
# 我们只传递位置信息(键),不传递值,使用 ordering 参数 # 我们只传递位置信息(键),不传递值,使用 ordering 参数
if ordering and isinstance(next(iter(ordering.values()), None), str): if ordering:
values = list(ordering.values())
value = values[0]
if isinstance(value, str):
# ordering 的值是字符串,只使用键(位置信息)创建新的 OrderedDict # ordering 的值是字符串,只使用键(位置信息)创建新的 OrderedDict
# 传递 ordering 参数而不是 ordered_items让 Plate 自己创建 Well 对象 # 传递 ordering 参数而不是 ordered_items让 Plate 自己创建 Well 对象
items = None items = None
# 使用 ordering 参数,只包含位置信息(键) # 使用 ordering 参数,只包含位置信息(键)
ordering_param = collections.OrderedDict((k, None) for k in ordering.keys()) ordering_param = collections.OrderedDict((k, None) for k in ordering.keys())
elif value is None:
ordering_param = ordering
else: else:
# ordering 的值已经是对象,可以直接使用 # ordering 的值已经是对象,可以直接使用
items = ordering items = ordering
ordering_param = None ordering_param = None
else:
items = None
ordering_param = None
# 根据情况传递不同的参数 # 根据情况传递不同的参数
if items is not None: if items is not None:
@@ -713,7 +718,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract):
mix_liquid_height: Optional[float] = None, mix_liquid_height: Optional[float] = None,
delays: Optional[List[int]] = None, delays: Optional[List[int]] = None,
none_keys: List[str] = [], none_keys: List[str] = [],
): ) -> TransferLiquidReturn:
return await super().transfer_liquid( return await super().transfer_liquid(
sources, sources,
targets, targets,

View File

@@ -638,7 +638,7 @@ liquid_handler:
placeholder_keys: {} placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 吸头迭代函数。用于自动管理和切换吸头架中的吸头,实现批量实验中的吸头自动分配和追踪。该函数监控吸头使用状态,自动切换到下一个可用吸头位置,确保实验流程的连续性。适用于高通量实验、批量处理、自动化流水线等需要大量吸头管理的应用场景。 description: 吸头迭代函数。用于自动管理和切换枪头盒中的吸头,实现批量实验中的吸头自动分配和追踪。该函数监控吸头使用状态,自动切换到下一个可用吸头位置,确保实验流程的连续性。适用于高通量实验、批量处理、自动化流水线等需要大量吸头管理的应用场景。
properties: properties:
feedback: {} feedback: {}
goal: goal:
@@ -712,6 +712,43 @@ liquid_handler:
title: set_group参数 title: set_group参数
type: object type: object
type: UniLabJsonCommand 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: auto-set_tiprack:
feedback: {} feedback: {}
goal: {} goal: {}
@@ -721,7 +758,7 @@ liquid_handler:
placeholder_keys: {} placeholder_keys: {}
result: {} result: {}
schema: schema:
description: 吸头架设置函数。用于配置和初始化液体处理系统的吸头架信息,包括吸头架位置、类型、容量等参数。该函数建立吸头资源管理系统,为后续的吸头选择和使用提供基础配置。适用于系统初始化、吸头架更换、实验配置等需要吸头资源管理的操作场景。 description: 枪头盒设置函数。用于配置和初始化液体处理系统的枪头盒信息,包括枪头盒位置、类型、容量等参数。该函数建立吸头资源管理系统,为后续的吸头选择和使用提供基础配置。适用于系统初始化、枪头盒更换、实验配置等需要吸头资源管理的操作场景。
properties: properties:
feedback: {} feedback: {}
goal: goal:
@@ -4093,32 +4130,32 @@ liquid_handler:
- 0 - 0
handles: handles:
input: input:
- data_key: liquid - data_key: sources
data_source: handle data_source: handle
data_type: resource data_type: resource
handler_key: sources handler_key: sources
label: sources label: 待移动液体
- data_key: liquid - data_key: targets
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
data_source: handle data_source: handle
data_type: resource 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 handler_key: sources_out
label: sources label: 移液后源孔
- data_key: liquid - data_key: targets.@flatten
data_source: executor data_source: executor
data_type: resource data_type: resource
handler_key: targets_out handler_key: targets_out
label: targets label: 移液后目标孔
placeholder_keys: placeholder_keys:
sources: unilabos_resources sources: unilabos_resources
targets: unilabos_resources targets: unilabos_resources
@@ -5114,19 +5151,34 @@ liquid_handler.biomek:
- 0 - 0
handles: handles:
input: input:
- data_key: liquid - data_key: sources
data_source: handle data_source: handle
data_type: resource data_type: resource
handler_key: liquid-input handler_key: sources
io_type: target 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: output:
- data_key: liquid - data_key: sources.@flatten
data_source: executor data_source: executor
data_type: resource data_type: resource
handler_key: liquid-output handler_key: sources_out
io_type: source io_type: source
label: Liquid Output label: 移液后源孔
- data_key: targets.@flatten
data_source: executor
data_type: resource
handler_key: targets_out
label: 移液后目标孔
placeholder_keys: placeholder_keys:
sources: unilabos_resources sources: unilabos_resources
targets: unilabos_resources targets: unilabos_resources
@@ -9924,18 +9976,18 @@ liquid_handler.prcxi:
data_source: handle data_source: handle
data_type: resource data_type: resource
handler_key: tip_rack_identifier handler_key: tip_rack_identifier
label: 头盒 label: 头盒
output: output:
- data_key: liquid - data_key: sources.@flatten
data_source: handle data_source: executor
data_type: resource data_type: resource
handler_key: sources_out handler_key: sources_out
label: sources label: 移液后源孔
- data_key: liquid - data_key: targets.@flatten
data_source: executor data_source: executor
data_type: resource data_type: resource
handler_key: targets_out handler_key: targets_out
label: targets label: 移液后目标孔
placeholder_keys: placeholder_keys:
sources: unilabos_resources sources: unilabos_resources
targets: unilabos_resources targets: unilabos_resources

View File

@@ -1581,7 +1581,7 @@ class BaseROS2DeviceNode(Node, Generic[T]):
f"转换ResourceSlot列表参数 {arg_name} 失败: {e}\n{traceback.format_exc()}" f"转换ResourceSlot列表参数 {arg_name} 失败: {e}\n{traceback.format_exc()}"
) )
raise JsonCommandInitError(f"ResourceSlot列表参数转换失败: {arg_name}") raise JsonCommandInitError(f"ResourceSlot列表参数转换失败: {arg_name}")
# todo: 默认反报送
return function(**function_args) return function(**function_args)
except KeyError as ex: except KeyError as ex:
raise JsonCommandInitError( raise JsonCommandInitError(
@@ -1614,8 +1614,8 @@ class BaseROS2DeviceNode(Node, Generic[T]):
timeout = 30.0 timeout = 30.0
elapsed = 0.0 elapsed = 0.0
while not future.done() and elapsed < timeout: while not future.done() and elapsed < timeout:
time.sleep(0.05) time.sleep(0.02)
elapsed += 0.05 elapsed += 0.02
if not future.done(): if not future.done():
raise Exception(f"资源查询超时: {uuids_list}") raise Exception(f"资源查询超时: {uuids_list}")

View File

@@ -807,7 +807,7 @@ class HostNode(BaseROS2DeviceNode):
assign_sample_id(action_kwargs) assign_sample_id(action_kwargs)
goal_msg = convert_to_ros_msg(action_client._action_type.Goal(), 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}: {action_kwargs}")
self.lab_logger().trace(f"[Host Node] Sending goal for {action_id}: {goal_msg}") self.lab_logger().trace(f"[Host Node] Sending goal for {action_id}: {goal_msg}")
action_client.wait_for_server() action_client.wait_for_server()