From ce8667f937f1aa275902ae1e5a9e6cd5c8d2c91f Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Sun, 6 Jul 2025 18:39:40 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E4=B8=AD=E6=9E=90=E4=BB=AA?= =?UTF-8?q?=E5=99=A8=EF=BC=8C=E4=BB=A5=E5=8F=8A=E5=90=AF=E5=8A=A8=E7=A4=BA?= =?UTF-8?q?=E4=BE=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/experiments/prcxi.json | 190 +++++ .../liquid_handler_abstract.py | 2 +- .../devices/liquid_handling/prcxi/prcxi.py | 566 +++++++++++-- unilabos/registry/devices/liquid_handler.yaml | 510 ++++++++++++ unilabos/registry/devices/work_station.yaml | 750 +++++++++--------- unilabos/ros/nodes/base_device_node.py | 7 + unilabos/ros/utils/driver_creator.py | 6 +- 7 files changed, 1577 insertions(+), 454 deletions(-) create mode 100644 test/experiments/prcxi.json diff --git a/test/experiments/prcxi.json b/test/experiments/prcxi.json new file mode 100644 index 0000000..72ea23b --- /dev/null +++ b/test/experiments/prcxi.json @@ -0,0 +1,190 @@ +{ + "nodes": [ + { + "id": "PLR_STATION_PRCXI", + "name": "PLR_LH_TEST", + "parent": null, + "type": "device", + "class": "liquid_handler.prcxi", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "deck": { + "_resource_child_name": "deck", + "_resource_type": "unilabos.devices.liquid_handling.prcxi.prcxi:PRCXI9300Deck" + }, + "host": "127.0.0.1", + "port": 9999, + "timeout": 10.0, + "setup": false + }, + "data": {}, + "children": [ + "deck" + ] + }, + { + "id": "deck", + "name": "deck", + "sample_id": null, + "children": [ + "rackT1", + "plateT2", + "plateT3", + "rackT4", + "plateT5" + ], + "parent": "PLR_STATION_PRCXI", + "type": "device", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "PRCXI9300Deck" + }, + "data": {} + }, + { + "id": "rackT1", + "name": "rackT1", + "sample_id": null, + "children": [], + "parent": "deck", + "type": "device", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "PRCXI9300Container", + "size_x": 120.98, + "size_y": 82.12, + "size_z": 50.3 + }, + "data": { + "Material": { + "uuid": "80652665f6a54402b2408d50b40398df", + "Code": "ZX-001-1000", + "Name": "1000μL Tip头", + "SummaryName": "1000μL Tip头", + "PipetteHeight": 100, + "materialEnum": 1 + } + } + }, + { + "id": "plateT2", + "name": "plateT2", + "sample_id": null, + "children": [], + "parent": "deck", + "type": "device", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "PRCXI9300Container", + "size_x": 120.98, + "size_y": 82.12, + "size_z": 50.3 + }, + "data": { + "Material": { + "uuid": "57b1e4711e9e4a32b529f3132fc5931f" + } + } + }, + { + "id": "plateT3", + "name": "plateT3", + "sample_id": null, + "children": [], + "parent": "deck", + "type": "device", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "PRCXI9300Container", + "size_x": 120.98, + "size_y": 82.12, + "size_z": 50.3 + }, + "data": { + "Material": { + "uuid": "57b1e4711e9e4a32b529f3132fc5931f" + } + } + }, + { + "id": "rackT4", + "name": "rackT4", + "sample_id": null, + "children": [], + "parent": "deck", + "type": "device", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "PRCXI9300Container", + "size_x": 120.98, + "size_y": 82.12, + "size_z": 50.3 + }, + "data": { + "Material": { + "uuid": "80652665f6a54402b2408d50b40398df", + "Code": "ZX-001-1000", + "Name": "1000μL Tip头", + "SummaryName": "1000μL Tip头", + "PipetteHeight": 100, + "materialEnum": 1 + } + } + }, + { + "id": "plateT5", + "name": "plateT5", + "sample_id": null, + "children": [], + "parent": "deck", + "type": "device", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "PRCXI9300Container", + "size_x": 120.98, + "size_y": 82.12, + "size_z": 50.3 + }, + "data": { + "Material": { + "uuid": "57b1e4711e9e4a32b529f3132fc5931f" + } + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/unilabos/devices/liquid_handling/liquid_handler_abstract.py b/unilabos/devices/liquid_handling/liquid_handler_abstract.py index 4faa042..54257ac 100644 --- a/unilabos/devices/liquid_handling/liquid_handler_abstract.py +++ b/unilabos/devices/liquid_handling/liquid_handler_abstract.py @@ -183,7 +183,7 @@ class LiquidHandlerAbstract(LiquidHandler): 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_times: Optional[List[int]] = None, mix_vol: Optional[int] = None, mix_rate: Optional[int] = None, mix_liquid_height: Optional[float] = None, diff --git a/unilabos/devices/liquid_handling/prcxi/prcxi.py b/unilabos/devices/liquid_handling/prcxi/prcxi.py index 0093c0f..25180cf 100644 --- a/unilabos/devices/liquid_handling/prcxi/prcxi.py +++ b/unilabos/devices/liquid_handling/prcxi/prcxi.py @@ -1,17 +1,456 @@ -import socket, json, contextlib -from typing import Any, List, Dict, Optional +import collections +import contextlib +import json +import socket +import time +from typing import Any, List, Dict, Optional, TypedDict, Union, Sequence, Iterator, Literal + +from pylabrobot.liquid_handling import ( + LiquidHandlerBackend, + Pickup, + SingleChannelAspiration, + Drop, + SingleChannelDispense, + PickupTipRack, + DropTipRack, + MultiHeadAspirationPlate, +) +from pylabrobot.liquid_handling.standard import ( + MultiHeadAspirationContainer, + MultiHeadDispenseContainer, + MultiHeadDispensePlate, + ResourcePickup, + ResourceMove, + ResourceDrop, +) +from pylabrobot.resources import Tip, Deck, Plate, Well, TipRack, Resource, Container, Coordinate + +from unilabos.devices.liquid_handling.liquid_handler_abstract import LiquidHandlerAbstract class PRCXIError(RuntimeError): """Lilith 返回 Success=false 时抛出的业务异常""" -class PRCXI9300: +class Material(TypedDict): # 和Plate同关系 + uuid: str + Code: Optional[str] + Name: Optional[str] + SummaryName: Optional[str] + PipetteHeight: Optional[int] + materialEnum: Optional[int] - def __init__(self, host: str = "127.0.0.1", port: int = 9999, - timeout: float = 10.0) -> None: + +class WorkTablets(TypedDict): + Number: int + Code: str + Material: Dict[str, Any] + + +class MatrixInfo(TypedDict): + MatrixId: str + MatrixName: str + MatrixCount: int + WorkTablets: list[WorkTablets] + + +class PRCXI9300Deck(Deck): + """PRCXI 9300 的专用 Deck 类,继承自 Deck。 + + 该类定义了 PRCXI 9300 的工作台布局和槽位信息。 + """ + + def __init__(self, name: str, size_x: float, size_y: float, size_z: float): + super().__init__(name, size_x, size_y, size_z) + self.slots = [None] * 6 # PRCXI 9300 有 6 个槽位 + + +class PRCXI9300Container(Plate): + """PRCXI 9300 的专用 Deck 类,继承自 Deck。 + + 该类定义了 PRCXI 9300 的工作台布局和槽位信息。 + """ + + def __init__(self, name: str, size_x: float, size_y: float, size_z: float, category: str): + super().__init__(name, size_x, size_y, size_z, category=category, ordering=collections.OrderedDict()) + self._unilabos_state = {} + + def load_state(self, state: Dict[str, Any]) -> None: + """从给定的状态加载工作台信息。""" + super().load_state(state) + self._unilabos_state = state + + def serialize_state(self) -> Dict[str, Dict[str, Any]]: + data = super().serialize_state() + data.update(self._unilabos_state) + return data + + +class PRCXI9300Handler(LiquidHandlerAbstract): + def __init__(self, deck: Deck, host: str, port: int, timeout: float, setup=True): + tablets_info = [] + count = 0 + for child in deck.children: + if "Material" in child._unilabos_state: + count += 1 + tablets_info.append( + WorkTablets(Number=count, Code=f"T{count}", Material=child._unilabos_state["Material"]) + ) + self._unilabos_backend = PRCXI9300Backend(tablets_info, host, port, timeout, setup) + super().__init__(backend=self._unilabos_backend, deck=deck) + + async def create_protocol( + self, + protocol_name: str, + protocol_description: str, + protocol_version: str, + protocol_author: str, + protocol_date: str, + protocol_type: str, + none_keys: List[str] = [], + ): + self._unilabos_backend.create_protocol(protocol_name) + + async def run_protocol(self): + self._unilabos_backend.run_protocol() + + 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] = [], + ): + return await super().remove_liquid( + vols, + sources, + 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, + ) + + 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] = [], + ): + return await super().add_liquid( + asp_vols, + dis_vols, + reagent_sources, + 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, + delays=delays, + mix_time=mix_time, + mix_vol=mix_vol, + mix_rate=mix_rate, + mix_liquid_height=mix_liquid_height, + none_keys=none_keys, + ) + + async def transfer_liquid( + self, + sources: Sequence[Container], + targets: Sequence[Container], + tip_racks: Sequence[TipRack], + *, + use_channels: Optional[List[int]] = None, + asp_vols: Union[List[float], float], + dis_vols: Union[List[float], float], + asp_flow_rates: Optional[List[Optional[float]]] = None, + dis_flow_rates: Optional[List[Optional[float]]] = None, + offsets: Optional[List[Coordinate]] = None, + 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] = [], + ): + return await super().transfer_liquid( + sources, + targets, + tip_racks, + use_channels=use_channels, + asp_vols=asp_vols, + dis_vols=dis_vols, + 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, + ) + + async def custom_delay(self, seconds=0, msg=None): + return await super().custom_delay(seconds, msg) + + async def touch_tip(self, targets: Sequence[Container]): + return await super().touch_tip(targets) + + 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] = [], + ): + return await super().mix(targets, mix_time, mix_vol, height_to_bottom, offsets, mix_rate, none_keys) + + def iter_tips(self, tip_racks: Sequence[TipRack]) -> Iterator[Resource]: + return super().iter_tips(tip_racks) + + def set_tiprack(self, tip_racks: Sequence[TipRack]): + super().set_tiprack(tip_racks) + + async def move_to(self, well: Well, dis_to_top: float = 0, channel: int = 0): + return await super().move_to(well, dis_to_top, channel) + + +class PRCXI9300Backend(LiquidHandlerBackend): + """PRCXI 9300 的后端实现,继承自 LiquidHandlerBackend。 + + 该类提供了与 PRCXI 9300 设备进行通信的基本方法,包括方案管理、自动化控制、运行状态查询等。 + """ + + _num_channels = 8 # 默认通道数为 8 + matrix_info: MatrixInfo + protocol_name: str + steps_todo_list = [] + + def __init__( + self, + tablets_info: list[WorkTablets], + host: str = "127.0.0.1", + port: int = 9999, + timeout: float = 10.0, + setup=True, + ) -> None: + super().__init__() + self.tablets_info = tablets_info + self.api_client = PRCXI9300Api(host, port, timeout) self.host, self.port, self.timeout = host, port, timeout + self._num_channels = 8 + self._execute_setup = setup + def create_protocol(self, protocol_name): + self.protocol_name = protocol_name + self.steps_todo_list = [] + + def run_protocol(self): + run_time = time.time() + self.matrix_info = MatrixInfo( + MatrixId=f"matrix_{run_time}", + MatrixName=f"protocol_{run_time}", + MatrixCount=len(self.tablets_info), + WorkTablets=self.tablets_info, + ) + self.api_client.add_WorkTablet_Matrix(self.matrix_info) + solution_id = self.api_client.add_solution( + f"protocol_{run_time}", self.matrix_info["MatrixId"], self.steps_todo_list + ) + self.api_client.load_solution(solution_id) + self.api_client.start() + + @classmethod + def check_channels(cls, use_channels: List[int]) -> List[int]: + """检查通道是否符合要求,PRCXI9300Backend 只支持所有 8 个通道。""" + if use_channels != [0, 1, 2, 3, 4, 5, 6, 7]: + print("PRCXI9300Backend only supports all 8 channels, using default [0, 1, 2, 3, 4, 5, 6, 7].") + return [0, 1, 2, 3, 4, 5, 6, 7] + return use_channels + + async def setup(self): + await super().setup() + try: + if self._execute_setup: + self.api_client.call("IAutomation", "Reset") + except ConnectionRefusedError as e: + raise RuntimeError( + f"Failed to connect to PRCXI9300 API at {self.host}:{self.port}. " + "Please ensure the PRCXI9300 service is running." + ) from e + + async def stop(self): + self.api_client.call("IAutomation", "Stop") + + async def pick_up_tips(self, ops: List[Pickup], use_channels: List[int] = None): + """Pick up tips from the specified resource.""" + # 12列,要PickUp A-H 1 + # PlateNo = 1-6 # 2行3列 + PlateNo = 1 # 第一块板 + hole_col = 2 # 第二列的8个孔 + step = self.api_client.Load( + "Left", + dosage=0, + plate_no=PlateNo, + is_whole_plate=False, + hole_row=1, + hole_col=hole_col, + blending_times=0, + balance_height=0, + plate_or_hole=f"H{hole_col}-8,T{PlateNo}", + hole_numbers="1,2,3,4,5,6,7,8", + ) + self.steps_todo_list.append(step) + print("PRCXI9300Backend pick_up_tips logged.") + + async def drop_tips(self, ops: List[Drop], use_channels: List[int] = None): + PlateNo = 1 + hole_col = 2 + step = self.api_client.UnLoad( + "Left", + dosage=0, + plate_no=PlateNo, + is_whole_plate=False, + hole_row=1, + hole_col=hole_col, + blending_times=0, + balance_height=0, + plate_or_hole=f"H{hole_col}-8,T{PlateNo}", + hole_numbers="1,2,3,4,5,6,7,8", + ) + allow_drop = False + for s in self.steps_todo_list: + if s.get("Function") == "Load": + self.steps_todo_list.append(step) + allow_drop = True + break + if not allow_drop: + raise ValueError("No matching Load step found for drop_tips.") + print("PRCXI9300Backend drop_tips logged.") + + async def aspirate(self, ops: List[SingleChannelAspiration], use_channels: List[int] = None): + volumes = [op.volume for op in ops] + print(f"PRCXI9300Backend aspirate volumes: {volumes[0]} -> {int(volumes[0])} μL") + PlateNo = 1 + hole_col = 2 + step = self.api_client.Imbibing( + "Left", + dosage=int(volumes[0]), + plate_no=PlateNo, + is_whole_plate=False, + hole_row=1, + hole_col=hole_col, + blending_times=0, + balance_height=0, + plate_or_hole=f"H{hole_col}-8,T{PlateNo}", + hole_numbers="1,2,3,4,5,6,7,8", + ) + self.steps_todo_list.append(step) + + async def dispense(self, ops: List[SingleChannelDispense], use_channels: List[int] = None): + volumes = [op.volume for op in ops] + print(f"PRCXI9300Backend dispense volumes: {volumes[0]} -> {int(volumes[0])} μL") + PlateNo = 1 + hole_col = 2 + step = self.api_client.Tapping( + "Left", + dosage=int(volumes[0]), + plate_no=PlateNo, + is_whole_plate=False, + hole_row=1, + hole_col=hole_col, + blending_times=0, + balance_height=0, + plate_or_hole=f"H{hole_col}-8,T{PlateNo}", + hole_numbers="1,2,3,4,5,6,7,8", + ) + self.steps_todo_list.append(step) + + async def pick_up_tips96(self, pickup: PickupTipRack): + raise NotImplementedError("The PRCXI backend does not support the 96 head.") + + async def drop_tips96(self, drop: DropTipRack): + raise NotImplementedError("The PRCXI backend does not support the 96 head.") + + async def aspirate96(self, aspiration: Union[MultiHeadAspirationPlate, MultiHeadAspirationContainer]): + raise NotImplementedError("The Opentrons backend does not support the 96 head.") + + async def dispense96(self, dispense: Union[MultiHeadDispensePlate, MultiHeadDispenseContainer]): + raise NotImplementedError("The Opentrons backend does not support the 96 head.") + + async def pick_up_resource(self, pickup: ResourcePickup): + raise NotImplementedError("The Opentrons backend does not support the robotic arm.") + + async def move_picked_up_resource(self, move: ResourceMove): + raise NotImplementedError("The Opentrons backend does not support the robotic arm.") + + async def drop_resource(self, drop: ResourceDrop): + raise NotImplementedError("The Opentrons backend does not support the robotic arm.") + + def can_pick_up_tip(self, channel_idx: int, tip: Tip) -> bool: + return True # PRCXI9300Backend does not have tip compatibility issues + + def serialize(self) -> dict: + raise NotImplementedError() + + @property + def num_channels(self) -> int: + return self._num_channels + + +class PRCXI9300Api: + def __init__(self, host: str = "127.0.0.1", port: int = 9999, timeout: float = 10.0) -> None: + self.host, self.port, self.timeout = host, port, timeout @staticmethod def _len_prefix(n: int) -> bytes: @@ -30,17 +469,30 @@ class PRCXI9300: if not chunk: break if first: - chunk, first = chunk[8:], False + chunk, first = chunk[8:], False chunks.append(chunk) return b"".join(chunks).decode() - def _call(self, service: str, method: str, - params: Optional[list] = None) -> Any: + # ---------------------------------------------------- 方案相关(ISolution) + def list_solutions(self) -> List[Dict[str, Any]]: + """GetSolutionList""" + return self.call("ISolution", "GetSolutionList") + + def load_solution(self, solution_id: str) -> bool: + """LoadSolution""" + return self.call("ISolution", "LoadSolution", [solution_id]) + + def add_solution(self, name: str, matrix_id: str, steps: List[Dict[str, Any]]) -> str: + """AddSolution → 返回新方案 GUID""" + return self.call("ISolution", "AddSolution", [name, matrix_id, steps]) + + # ---------------------------------------------------- 自动化控制(IAutomation) + def start(self) -> bool: + return self.call("IAutomation", "Start") + + def call(self, service: str, method: str, params: Optional[list] = None) -> Any: payload = json.dumps( - {"ServiceName": service, - "MethodName": method, - "Paramters": params or []}, - separators=(",", ":") + {"ServiceName": service, "MethodName": method, "Paramters": params or []}, separators=(",", ":") ) resp = json.loads(self._raw_request(payload)) if not resp.get("Success", False): @@ -51,87 +503,53 @@ class PRCXI9300: except (TypeError, json.JSONDecodeError): return data - # ---------------------------------------------------- 方案相关(ISolution) - def list_solutions(self) -> List[Dict[str, Any]]: - """GetSolutionList""" - return self._call("ISolution", "GetSolutionList") - - def load_solution(self, solution_id: str) -> bool: - """LoadSolution""" - return self._call("ISolution", "LoadSolution", [solution_id]) - - def add_solution(self, name: str, matrix_id: str, - steps: List[Dict[str, Any]]) -> str: - """AddSolution → 返回新方案 GUID""" - return self._call("ISolution", "AddSolution", - [name, matrix_id, steps]) - - # ---------------------------------------------------- 自动化控制(IAutomation) - def start(self) -> bool: - return self._call("IAutomation", "Start") - - def stop(self) -> bool: - """Stop""" - return self._call("IAutomation", "Stop") - - def reset(self) -> bool: - """Reset""" - return self._call("IAutomation", "Reset") - def pause(self) -> bool: """Pause""" - return self._call("IAutomation", "Pause") + return self.call("IAutomation", "Pause") def resume(self) -> bool: """Resume""" - return self._call("IAutomation", "Resume") + return self.call("IAutomation", "Resume") def get_error_code(self) -> Optional[str]: """GetErrorCode""" - return self._call("IAutomation", "GetErrorCode") + return self.call("IAutomation", "GetErrorCode") def clear_error_code(self) -> bool: """RemoveErrorCodet""" - return self._call("IAutomation", "RemoveErrorCodet") + return self.call("IAutomation", "RemoveErrorCodet") # ---------------------------------------------------- 运行状态(IMachineState) def step_state_list(self) -> List[Dict[str, Any]]: """GetStepStateList""" - return self._call("IMachineState", "GetStepStateList") + return self.call("IMachineState", "GetStepStateList") def step_status(self, seq_num: int) -> Dict[str, Any]: """GetStepStatus""" - return self._call("IMachineState", "GetStepStatus", [seq_num]) + return self.call("IMachineState", "GetStepStatus", [seq_num]) def step_state(self, seq_num: int) -> Dict[str, Any]: """GetStepState""" - return self._call("IMachineState", "GetStepState", [seq_num]) + return self.call("IMachineState", "GetStepState", [seq_num]) def axis_location(self, axis_num: int = 1) -> Dict[str, Any]: """GetLocation""" - return self._call("IMachineState", "GetLocation", [axis_num]) + return self.call("IMachineState", "GetLocation", [axis_num]) # ---------------------------------------------------- 版位矩阵(IMatrix) def list_matrices(self) -> List[Dict[str, Any]]: """GetWorkTabletMatrices""" - return self._call("IMatrix", "GetWorkTabletMatrices") + return self.call("IMatrix", "GetWorkTabletMatrices") def matrix_by_id(self, matrix_id: str) -> Dict[str, Any]: """GetWorkTabletMatrixById""" - return self._call("IMatrix", "GetWorkTabletMatrixById", [matrix_id]) - - def add_WorkTablet_Matrix(self,matrix): - return self._call("IMatrix", "AddWorkTabletMatrix", [matrix]) + return self.call("IMatrix", "GetWorkTabletMatrixById", [matrix_id]) - # ---------------------------------------------------- 一键运行 - def run_solution(self, solution_id: str, channel_idx: int = 1) -> None: - self.load_solution(solution_id) - self.start(channel_idx) - -# ---------------------------------------------------- 单点动作 + def add_WorkTablet_Matrix(self, matrix: MatrixInfo): + return self.call("IMatrix", "AddWorkTabletMatrix", [matrix]) def Load( - self, + self, axis: str, dosage: int, plate_no: int, @@ -147,7 +565,7 @@ class PRCXI9300: assist_fun3: str = "", assist_fun4: str = "", assist_fun5: str = "", - liquid_method: str = "NormalDispense" + liquid_method: str = "NormalDispense", ) -> Dict[str, Any]: return { "StepAxis": axis, @@ -166,11 +584,11 @@ class PRCXI9300: "AssistFun4": assist_fun4, "AssistFun5": assist_fun5, "HoleNumbers": hole_numbers, - "LiquidDispensingMethod": liquid_method + "LiquidDispensingMethod": liquid_method, } def Imbibing( - self, + self, axis: str, dosage: int, plate_no: int, @@ -186,7 +604,7 @@ class PRCXI9300: assist_fun3: str = "", assist_fun4: str = "", assist_fun5: str = "", - liquid_method: str = "NormalDispense" + liquid_method: str = "NormalDispense", ) -> Dict[str, Any]: return { "StepAxis": axis, @@ -205,12 +623,11 @@ class PRCXI9300: "AssistFun4": assist_fun4, "AssistFun5": assist_fun5, "HoleNumbers": hole_numbers, - "LiquidDispensingMethod": liquid_method + "LiquidDispensingMethod": liquid_method, } - def Tapping( - self, + self, axis: str, dosage: int, plate_no: int, @@ -226,7 +643,7 @@ class PRCXI9300: assist_fun3: str = "", assist_fun4: str = "", assist_fun5: str = "", - liquid_method: str = "NormalDispense" + liquid_method: str = "NormalDispense", ) -> Dict[str, Any]: return { "StepAxis": axis, @@ -245,12 +662,11 @@ class PRCXI9300: "AssistFun4": assist_fun4, "AssistFun5": assist_fun5, "HoleNumbers": hole_numbers, - "LiquidDispensingMethod": liquid_method + "LiquidDispensingMethod": liquid_method, } - def Blending( - self, + self, axis: str, dosage: int, plate_no: int, @@ -266,7 +682,7 @@ class PRCXI9300: assist_fun3: str = "", assist_fun4: str = "", assist_fun5: str = "", - liquid_method: str = "NormalDispense" + liquid_method: str = "NormalDispense", ) -> Dict[str, Any]: return { "StepAxis": axis, @@ -285,11 +701,11 @@ class PRCXI9300: "AssistFun4": assist_fun4, "AssistFun5": assist_fun5, "HoleNumbers": hole_numbers, - "LiquidDispensingMethod": liquid_method + "LiquidDispensingMethod": liquid_method, } def UnLoad( - self, + self, axis: str, dosage: int, plate_no: int, @@ -305,7 +721,7 @@ class PRCXI9300: assist_fun3: str = "", assist_fun4: str = "", assist_fun5: str = "", - liquid_method: str = "NormalDispense" + liquid_method: str = "NormalDispense", ) -> Dict[str, Any]: return { "StepAxis": axis, @@ -324,5 +740,5 @@ class PRCXI9300: "AssistFun4": assist_fun4, "AssistFun5": assist_fun5, "HoleNumbers": hole_numbers, - "LiquidDispensingMethod": liquid_method + "LiquidDispensingMethod": liquid_method, } diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml index ea14223..046c52b 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -6064,6 +6064,516 @@ liquid_handler.biomek: required: - success type: object +liquid_handler.prcxi: + class: + action_value_mappings: + auto-add_liquid: + feedback: {} + goal: {} + goal_default: + asp_vols: null + blow_out_air_volume: null + delays: null + dis_vols: null + flow_rates: null + is_96_well: false + liquid_height: null + mix_liquid_height: null + mix_rate: null + mix_time: null + mix_vol: null + none_keys: [] + offsets: null + reagent_sources: null + spread: wide + targets: null + use_channels: null + handles: [] + result: {} + schema: + description: add_liquid的参数schema + properties: + feedback: {} + goal: + properties: + asp_vols: + type: string + blow_out_air_volume: + type: string + delays: + type: string + dis_vols: + type: string + flow_rates: + type: string + is_96_well: + default: false + type: boolean + liquid_height: + type: string + mix_liquid_height: + type: string + mix_rate: + type: string + mix_time: + type: string + mix_vol: + type: string + none_keys: + default: [] + type: array + offsets: + type: string + reagent_sources: + type: string + spread: + default: wide + type: string + targets: + type: string + use_channels: + type: string + required: + - asp_vols + - dis_vols + - reagent_sources + - targets + type: object + result: {} + required: + - goal + title: add_liquid参数 + type: object + type: UniLabJsonCommandAsync + auto-create_protocol: + feedback: {} + goal: {} + goal_default: + none_keys: [] + protocol_author: null + protocol_date: null + protocol_description: null + protocol_name: null + protocol_type: null + protocol_version: null + handles: [] + result: {} + schema: + description: create_protocol的参数schema + properties: + feedback: {} + goal: + properties: + none_keys: + default: [] + type: array + protocol_author: + type: string + protocol_date: + type: string + protocol_description: + type: string + protocol_name: + type: string + protocol_type: + type: string + protocol_version: + type: string + required: + - protocol_name + - protocol_description + - protocol_version + - protocol_author + - protocol_date + - protocol_type + type: object + result: {} + required: + - goal + title: create_protocol参数 + type: object + type: UniLabJsonCommandAsync + auto-custom_delay: + feedback: {} + goal: {} + goal_default: + msg: null + seconds: 0 + handles: [] + result: {} + schema: + description: custom_delay的参数schema + properties: + feedback: {} + goal: + properties: + msg: + type: string + seconds: + default: 0 + type: string + required: [] + type: object + result: {} + required: + - goal + title: custom_delay参数 + type: object + type: UniLabJsonCommandAsync + auto-iter_tips: + feedback: {} + goal: {} + goal_default: + tip_racks: null + handles: [] + result: {} + schema: + description: iter_tips的参数schema + properties: + feedback: {} + goal: + properties: + tip_racks: + type: string + required: + - tip_racks + type: object + result: {} + required: + - goal + title: iter_tips参数 + type: object + type: UniLabJsonCommand + auto-mix: + feedback: {} + goal: {} + goal_default: + height_to_bottom: null + mix_rate: null + mix_time: null + mix_vol: null + none_keys: [] + offsets: null + targets: null + handles: [] + result: {} + schema: + description: mix的参数schema + properties: + feedback: {} + goal: + properties: + height_to_bottom: + type: string + mix_rate: + type: string + mix_time: + type: integer + mix_vol: + type: string + none_keys: + default: [] + type: array + offsets: + type: string + targets: + type: string + required: + - targets + type: object + result: {} + required: + - goal + title: mix参数 + type: object + type: UniLabJsonCommandAsync + auto-move_to: + feedback: {} + goal: {} + goal_default: + channel: 0 + dis_to_top: 0 + well: null + handles: [] + result: {} + schema: + description: move_to的参数schema + properties: + feedback: {} + goal: + properties: + channel: + default: 0 + type: integer + dis_to_top: + default: 0 + type: number + well: + type: string + required: + - well + type: object + result: {} + required: + - goal + title: move_to参数 + type: object + type: UniLabJsonCommandAsync + auto-remove_liquid: + feedback: {} + goal: {} + goal_default: + blow_out_air_volume: null + delays: null + flow_rates: null + is_96_well: false + liquid_height: null + none_keys: [] + offsets: null + sources: null + spread: wide + top: null + use_channels: null + vols: null + waste_liquid: null + handles: [] + result: {} + schema: + description: remove_liquid的参数schema + properties: + feedback: {} + goal: + properties: + blow_out_air_volume: + type: string + delays: + type: string + flow_rates: + type: string + is_96_well: + default: false + type: string + liquid_height: + type: string + none_keys: + default: [] + type: array + offsets: + type: string + sources: + type: string + spread: + default: wide + type: string + top: + type: string + use_channels: + type: string + vols: + type: array + waste_liquid: + type: string + required: + - vols + - sources + type: object + result: {} + required: + - goal + title: remove_liquid参数 + type: object + type: UniLabJsonCommandAsync + auto-run_protocol: + feedback: {} + goal: {} + goal_default: {} + handles: [] + result: {} + schema: + description: run_protocol的参数schema + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: run_protocol参数 + type: object + type: UniLabJsonCommandAsync + auto-set_tiprack: + feedback: {} + goal: {} + goal_default: + tip_racks: null + handles: [] + result: {} + schema: + description: set_tiprack的参数schema + properties: + feedback: {} + goal: + properties: + tip_racks: + type: string + required: + - tip_racks + type: object + result: {} + required: + - goal + title: set_tiprack参数 + type: object + type: UniLabJsonCommand + auto-touch_tip: + feedback: {} + goal: {} + goal_default: + targets: null + handles: [] + result: {} + schema: + description: touch_tip的参数schema + properties: + feedback: {} + goal: + properties: + targets: + type: string + required: + - targets + type: object + result: {} + required: + - goal + title: touch_tip参数 + type: object + type: UniLabJsonCommandAsync + auto-transfer_liquid: + feedback: {} + goal: {} + goal_default: + asp_flow_rates: null + asp_vols: null + blow_out_air_volume: null + delays: null + dis_flow_rates: null + dis_vols: null + is_96_well: false + liquid_height: null + mix_liquid_height: null + mix_rate: null + mix_stage: none + mix_times: null + mix_vol: null + none_keys: [] + offsets: null + sources: null + spread: wide + targets: null + tip_racks: null + touch_tip: false + use_channels: null + handles: [] + result: {} + schema: + description: transfer_liquid的参数schema + properties: + feedback: {} + goal: + properties: + asp_flow_rates: + type: string + asp_vols: + type: string + blow_out_air_volume: + type: string + delays: + type: string + dis_flow_rates: + type: string + dis_vols: + type: string + is_96_well: + default: false + type: boolean + liquid_height: + type: string + mix_liquid_height: + type: string + mix_rate: + type: string + mix_stage: + default: none + type: string + mix_times: + type: string + mix_vol: + type: string + none_keys: + default: [] + type: array + offsets: + type: string + sources: + type: string + spread: + default: wide + type: string + targets: + type: string + tip_racks: + type: string + touch_tip: + default: false + type: boolean + use_channels: + type: string + required: + - sources + - targets + - tip_racks + - asp_vols + - dis_vols + type: object + result: {} + required: + - goal + title: transfer_liquid参数 + type: object + type: UniLabJsonCommandAsync + module: unilabos.devices.liquid_handling.prcxi.prcxi:PRCXI9300Handler + status_types: {} + type: python + description: prcxi液体处理器设备,基于pylabrobot控制 + handles: [] + icon: icon_yiyezhan.webp + init_param_schema: + config: + properties: + deck: + type: string + host: + type: string + port: + type: integer + setup: + default: true + type: string + timeout: + type: number + required: + - deck + - host + - port + - timeout + type: object + data: + properties: {} + required: [] + type: object liquid_handler.revvity: class: action_value_mappings: diff --git a/unilabos/registry/devices/work_station.yaml b/unilabos/registry/devices/work_station.yaml index 5941645..ed8b86f 100644 --- a/unilabos/registry/devices/work_station.yaml +++ b/unilabos/registry/devices/work_station.yaml @@ -356,6 +356,85 @@ workstation: title: Add type: object type: Add + AdjustPHProtocol: + feedback: {} + goal: + ph_value: ph_value + reagent: reagent + vessel: vessel + goal_default: + ph_value: 0.0 + reagent: '' + vessel: '' + handles: + input: + - data_key: vessel + data_source: handle + data_type: resource + handler_key: Vessel + label: Vessel + - data_key: reagent + data_source: handle + data_type: resource + handler_key: reagent + label: Reagent + output: + - data_key: vessel + data_source: executor + data_type: resource + handler_key: VesselOut + label: Vessel + result: {} + schema: + description: ROS Action AdjustPH 的 JSON Schema + properties: + feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 + properties: + progress: + type: number + status: + type: string + required: + - status + - progress + title: AdjustPH_Feedback + type: object + goal: + description: Action 目标 - 从客户端发送到服务器 + properties: + ph_value: + type: number + reagent: + type: string + vessel: + type: string + required: + - vessel + - ph_value + - reagent + title: AdjustPH_Goal + type: object + result: + description: Action 结果 - 完成后从服务器发送到客户端 + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: AdjustPH_Result + type: object + required: + - goal + title: AdjustPH + type: object + type: AdjustPH CentrifugeProtocol: feedback: {} goal: @@ -751,6 +830,75 @@ workstation: title: Dissolve type: object type: Dissolve + DryProtocol: + feedback: {} + goal: + compound: compound + vessel: vessel + goal_default: + compound: '' + vessel: '' + handles: + input: + - data_key: vessel + data_source: handle + data_type: resource + handler_key: Vessel + label: Vessel + output: + - data_key: vessel + data_source: executor + data_type: resource + handler_key: VesselOut + label: Vessel + result: {} + schema: + description: ROS Action Dry 的 JSON Schema + properties: + feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 + properties: + progress: + type: number + status: + type: string + required: + - status + - progress + title: Dry_Feedback + type: object + goal: + description: Action 目标 - 从客户端发送到服务器 + properties: + compound: + type: string + vessel: + type: string + required: + - compound + - vessel + title: Dry_Goal + type: object + result: + description: Action 结果 - 完成后从服务器发送到客户端 + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: Dry_Result + type: object + required: + - goal + title: Dry + type: object + type: Dry EvacuateAndRefillProtocol: feedback: {} goal: @@ -1399,6 +1547,80 @@ workstation: title: HeatChillStop type: object type: HeatChillStop + HydrogenateProtocol: + feedback: {} + goal: + temp: temp + time: time + vessel: vessel + goal_default: + temp: '' + time: '' + vessel: '' + handles: + input: + - data_key: vessel + data_source: handle + data_type: resource + handler_key: Vessel + label: Vessel + output: + - data_key: vessel + data_source: executor + data_type: resource + handler_key: VesselOut + label: Vessel + result: {} + schema: + description: ROS Action Hydrogenate 的 JSON Schema + properties: + feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 + properties: + progress: + type: number + status: + type: string + required: + - status + - progress + title: Hydrogenate_Feedback + type: object + goal: + description: Action 目标 - 从客户端发送到服务器 + properties: + temp: + type: string + time: + type: string + vessel: + type: string + required: + - temp + - time + - vessel + title: Hydrogenate_Goal + type: object + result: + description: Action 结果 - 完成后从服务器发送到客户端 + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: Hydrogenate_Result + type: object + required: + - goal + title: Hydrogenate + type: object + type: Hydrogenate PumpTransferProtocol: feedback: {} goal: @@ -1554,6 +1776,159 @@ workstation: title: PumpTransfer type: object type: PumpTransfer + RecrystallizeProtocol: + feedback: {} + goal: + ratio: ratio + solvent1: solvent1 + solvent2: solvent2 + vessel: vessel + volume: volume + goal_default: + ratio: '' + solvent1: '' + solvent2: '' + vessel: '' + volume: 0.0 + handles: + input: + - data_key: vessel + data_source: handle + data_type: resource + handler_key: Vessel + label: Vessel + - data_key: solvent + data_source: handle + data_type: resource + handler_key: solvent1 + label: Solvent 1 + - data_key: solvent + data_source: handle + data_type: resource + handler_key: solvent2 + label: Solvent 2 + output: + - data_key: vessel + data_source: executor + data_type: resource + handler_key: VesselOut + label: Vessel + result: {} + schema: + description: ROS Action Recrystallize 的 JSON Schema + properties: + feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 + properties: + progress: + type: number + status: + type: string + required: + - status + - progress + title: Recrystallize_Feedback + type: object + goal: + description: Action 目标 - 从客户端发送到服务器 + properties: + ratio: + type: string + solvent1: + type: string + solvent2: + type: string + vessel: + type: string + volume: + type: number + required: + - ratio + - solvent1 + - solvent2 + - vessel + - volume + title: Recrystallize_Goal + type: object + result: + description: Action 结果 - 完成后从服务器发送到客户端 + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: Recrystallize_Result + type: object + required: + - goal + title: Recrystallize + type: object + type: Recrystallize + ResetHandlingProtocol: + feedback: {} + goal: + solvent: solvent + goal_default: + solvent: '' + handles: + input: + - data_key: solvent + data_source: handle + data_type: resource + handler_key: solvent + label: Solvent + output: [] + result: {} + schema: + description: ROS Action ResetHandling 的 JSON Schema + properties: + feedback: + description: Action 反馈 - 执行过程中从服务器发送到客户端 + properties: + progress: + type: number + status: + type: string + required: + - status + - progress + title: ResetHandling_Feedback + type: object + goal: + description: Action 目标 - 从客户端发送到服务器 + properties: + solvent: + type: string + required: + - solvent + title: ResetHandling_Goal + type: object + result: + description: Action 结果 - 完成后从服务器发送到客户端 + properties: + message: + type: string + return_info: + type: string + success: + type: boolean + required: + - success + - message + - return_info + title: ResetHandling_Result + type: object + required: + - goal + title: ResetHandling + type: object + type: ResetHandling RunColumnProtocol: feedback: {} goal: @@ -2355,381 +2730,6 @@ workstation: title: initialize_device参数 type: object type: UniLabJsonCommand - AdjustPHProtocol: - feedback: {} - goal: - vessel: vessel - ph_value: ph_value - reagent: reagent - goal_default: - vessel: '' - ph_value: 7.0 - reagent: '' - handles: - input: - - data_key: vessel - data_source: handle - data_type: resource - handler_key: Vessel - label: Vessel - - data_key: reagent - data_source: handle - data_type: resource - handler_key: reagent - label: Reagent - output: - - data_key: vessel - data_source: executor - data_type: resource - handler_key: VesselOut - label: Vessel - result: {} - schema: - description: ROS Action AdjustPH 的 JSON Schema - properties: - feedback: - description: Action 反馈 - 执行过程中从服务器发送到客户端 - properties: - status: - type: string - progress: - type: number - required: - - status - - progress - title: AdjustPH_Feedback - type: object - goal: - description: Action 目标 - 从客户端发送到服务器 - properties: - vessel: - type: string - ph_value: - type: number - reagent: - type: string - required: - - vessel - - ph_value - - reagent - title: AdjustPH_Goal - type: object - result: - description: Action 结果 - 完成后从服务器发送到客户端 - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: AdjustPH_Result - type: object - required: - - goal - title: AdjustPH - type: object - type: AdjustPH - ResetHandlingProtocol: - feedback: {} - goal: - solvent: solvent - goal_default: - solvent: '' - handles: - input: - - data_key: solvent - data_source: handle - data_type: resource - handler_key: solvent - label: Solvent - output: [] - result: {} - schema: - description: ROS Action ResetHandling 的 JSON Schema - properties: - feedback: - description: Action 反馈 - 执行过程中从服务器发送到客户端 - properties: - status: - type: string - progress: - type: number - required: - - status - - progress - title: ResetHandling_Feedback - type: object - goal: - description: Action 目标 - 从客户端发送到服务器 - properties: - solvent: - type: string - required: - - solvent - title: ResetHandling_Goal - type: object - result: - description: Action 结果 - 完成后从服务器发送到客户端 - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: ResetHandling_Result - type: object - required: - - goal - title: ResetHandling - type: object - type: ResetHandling - DryProtocol: - feedback: {} - goal: - compound: compound - vessel: vessel - goal_default: - compound: '' - vessel: '' - handles: - input: - - data_key: vessel - data_source: handle - data_type: resource - handler_key: Vessel - label: Vessel - output: - - data_key: vessel - data_source: executor - data_type: resource - handler_key: VesselOut - label: Vessel - result: {} - schema: - description: ROS Action Dry 的 JSON Schema - properties: - feedback: - description: Action 反馈 - 执行过程中从服务器发送到客户端 - properties: - status: - type: string - progress: - type: number - required: - - status - - progress - title: Dry_Feedback - type: object - goal: - description: Action 目标 - 从客户端发送到服务器 - properties: - compound: - type: string - vessel: - type: string - required: - - compound - - vessel - title: Dry_Goal - type: object - result: - description: Action 结果 - 完成后从服务器发送到客户端 - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Dry_Result - type: object - required: - - goal - title: Dry - type: object - type: Dry - HydrogenateProtocol: - feedback: {} - goal: - temp: temp - time: time - vessel: vessel - goal_default: - temp: '' - time: '' - vessel: '' - handles: - input: - - data_key: vessel - data_source: handle - data_type: resource - handler_key: Vessel - label: Vessel - output: - - data_key: vessel - data_source: executor - data_type: resource - handler_key: VesselOut - label: Vessel - result: {} - schema: - description: ROS Action Hydrogenate 的 JSON Schema - properties: - feedback: - description: Action 反馈 - 执行过程中从服务器发送到客户端 - properties: - status: - type: string - progress: - type: number - required: - - status - - progress - title: Hydrogenate_Feedback - type: object - goal: - description: Action 目标 - 从客户端发送到服务器 - properties: - temp: - type: string - time: - type: string - vessel: - type: string - required: - - temp - - time - - vessel - title: Hydrogenate_Goal - type: object - result: - description: Action 结果 - 完成后从服务器发送到客户端 - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Hydrogenate_Result - type: object - required: - - goal - title: Hydrogenate - type: object - type: Hydrogenate - RecrystallizeProtocol: - feedback: {} - goal: - ratio: ratio - solvent1: solvent1 - solvent2: solvent2 - vessel: vessel - volume: volume - goal_default: - ratio: '' - solvent1: '' - solvent2: '' - vessel: '' - volume: 0.0 - handles: - input: - - data_key: vessel - data_source: handle - data_type: resource - handler_key: Vessel - label: Vessel - - data_key: solvent - data_source: handle - data_type: resource - handler_key: solvent1 - label: Solvent 1 - - data_key: solvent - data_source: handle - data_type: resource - handler_key: solvent2 - label: Solvent 2 - output: - - data_key: vessel - data_source: executor - data_type: resource - handler_key: VesselOut - label: Vessel - result: {} - schema: - description: ROS Action Recrystallize 的 JSON Schema - properties: - feedback: - description: Action 反馈 - 执行过程中从服务器发送到客户端 - properties: - status: - type: string - progress: - type: number - required: - - status - - progress - title: Recrystallize_Feedback - type: object - goal: - description: Action 目标 - 从客户端发送到服务器 - properties: - ratio: - type: string - solvent1: - type: string - solvent2: - type: string - vessel: - type: string - volume: - type: number - required: - - ratio - - solvent1 - - solvent2 - - vessel - - volume - title: Recrystallize_Goal - type: object - result: - description: Action 结果 - 完成后从服务器发送到客户端 - properties: - message: - type: string - return_info: - type: string - success: - type: boolean - required: - - success - - message - - return_info - title: Recrystallize_Result - type: object - required: - - goal - title: Recrystallize - type: object - type: Recrystallize module: unilabos.ros.nodes.presets.protocol_node:ROS2ProtocolNode status_types: {} type: ros2 diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index 4b7eb07..d4ea392 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -923,11 +923,18 @@ class ROS2DeviceNode: driver_class.__module__.startswith("pylabrobot") or driver_class.__name__ == "LiquidHandlerAbstract" or driver_class.__name__ == "LiquidHandlerBiomek" + or driver_class.__name__ == "PRCXI9300Handler" ) # TODO: 要在创建之前预先请求服务器是否有当前id的物料,放到resource_tracker中,让pylabrobot进行创建 # 创建设备类实例 if use_pylabrobot_creator: + # 先对pylabrobot的子资源进行加载,不然subclass无法认出 + # 在下方对于加载Deck等Resource要手动import + # noinspection PyUnresolvedReferences + from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Deck + # noinspection PyUnresolvedReferences + from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Container self._driver_creator = PyLabRobotCreator( driver_class, children=children, resource_tracker=self.resource_tracker ) diff --git a/unilabos/ros/utils/driver_creator.py b/unilabos/ros/utils/driver_creator.py index 7fd726b..c76cc3b 100644 --- a/unilabos/ros/utils/driver_creator.py +++ b/unilabos/ros/utils/driver_creator.py @@ -148,7 +148,7 @@ class PyLabRobotCreator(DeviceClassCreator[T]): contain_model = not issubclass(target_type, Deck) resource, target_type = self._process_resource_mapping(resource, target_type) resource_instance: Resource = resource_ulab_to_plr(resource, contain_model) - + states[prefix_path] = resource_instance.serialize_all_state() # 使用 prefix_path 作为 key 存储资源状态 if to_dict: serialized = resource_instance.serialize() @@ -199,7 +199,7 @@ class PyLabRobotCreator(DeviceClassCreator[T]): spect = inspect.signature(deserialize_method) spec_args = spect.parameters for param_name, param_value in data.copy().items(): - if "_resource_child_name" in param_value and "_resource_type" not in param_value: + if isinstance(param_value, dict) and "_resource_child_name" in param_value and "_resource_type" not in param_value: arg_value = spec_args[param_name].annotation data[param_name]["_resource_type"] = self.device_cls.__module__ + ":" + arg_value logger.debug(f"自动补充 _resource_type: {data[param_name]['_resource_type']}") @@ -230,7 +230,7 @@ class PyLabRobotCreator(DeviceClassCreator[T]): spect = inspect.signature(self.device_cls.__init__) spec_args = spect.parameters for param_name, param_value in data.copy().items(): - if "_resource_child_name" in param_value and "_resource_type" not in param_value: + if isinstance(param_value, dict) and "_resource_child_name" in param_value and "_resource_type" not in param_value: arg_value = spec_args[param_name].annotation data[param_name]["_resource_type"] = self.device_cls.__module__ + ":" + arg_value logger.debug(f"自动补充 _resource_type: {data[param_name]['_resource_type']}")