From 9b706236f667ef0d0e3ae5f014dcb669fccd5fe1 Mon Sep 17 00:00:00 2001 From: q434343 <554662886@qq.com> Date: Thu, 2 Apr 2026 12:44:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E7=89=A9=E6=96=99=E4=BD=8D?= =?UTF-8?q?=E7=BD=AE=E6=A0=87=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../liquid_handler_abstract.py | 2 +- .../devices/liquid_handling/prcxi/prcxi.py | 16 +- unilabos/resources/resource_tracker.py | 13 +- unilabos/ros/nodes/base_device_node.py | 57 ++- .../test/experiments/prcxi_9300_slim.json | 1 + .../test/experiments/prcxi_9320_slim.json | 436 +----------------- unilabos/workflow/common.py | 2 +- unilabos/workflow/convert_from_json.py | 4 +- .../legacy/convert_from_json_legacy.py | 2 +- 9 files changed, 80 insertions(+), 453 deletions(-) diff --git a/unilabos/devices/liquid_handling/liquid_handler_abstract.py b/unilabos/devices/liquid_handling/liquid_handler_abstract.py index a10e1eed..fdb82806 100644 --- a/unilabos/devices/liquid_handling/liquid_handler_abstract.py +++ b/unilabos/devices/liquid_handling/liquid_handler_abstract.py @@ -1651,7 +1651,7 @@ class LiquidHandlerAbstract(LiquidHandlerMiddleware): tip = [] if pick_up: tip.append(self._get_next_tip()) - await self.pick_up_tips(tip) + await self.pick_up_tips(tip,use_channels=use_channels) blow_out_air_volume_before_vol = 0.0 if blow_out_air_volume_before is not None and len(blow_out_air_volume_before) > 0: blow_out_air_volume_before_vol = float(blow_out_air_volume_before[0] or 0.0) diff --git a/unilabos/devices/liquid_handling/prcxi/prcxi.py b/unilabos/devices/liquid_handling/prcxi/prcxi.py index c8bb83cf..de657749 100644 --- a/unilabos/devices/liquid_handling/prcxi/prcxi.py +++ b/unilabos/devices/liquid_handling/prcxi/prcxi.py @@ -781,9 +781,9 @@ class PRCXI9300Handler(LiquidHandlerAbstract): rail_interval=0, x_increase = -0.003636, y_increase = -0.003636, - x_offset = 9.2, - y_offset = -27.98, - deck_z = 300, + x_offset = -1.8, + y_offset = -37.48, + deck_z = 309.5, deck_y = 400, rail_width=27.5, xy_coupling = -0.0045, @@ -798,7 +798,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract): self.y_offset = y_offset self.xy_coupling = xy_coupling self.left_2_claw = Coordinate(-130.2, 34, -134) - self.right_2_left = Coordinate(22,-1, 8) + self.right_2_left = Coordinate(22,-1, 11) plate_positions = [] tablets_info = [] @@ -930,7 +930,7 @@ class PRCXI9300Handler(LiquidHandlerAbstract): matrix_id = str(uuid.uuid4()) matrix_info = { "MatrixId": matrix_id, - "MatrixName": matrix_id, + "MatrixName": "matrix_" + str(time.time()), "WorkTablets": work_tablets + [{"Number": number, "Material": {"uuid": "730067cf07ae43849ddf4034299030e9"}} for number in slot_none], } @@ -1152,6 +1152,12 @@ class PRCXI9300Handler(LiquidHandlerAbstract): self._first_transfer_done = True if self.step_mode: await self.create_protocol(f"transfer_liquid{time.time()}") + + _asp_list = asp_vols if isinstance(asp_vols, list) else [asp_vols] + _dis_list = dis_vols if isinstance(dis_vols, list) else [dis_vols] + if all(v <= 10.0 for v in _asp_list) and all(v <= 10.0 for v in _dis_list): + use_channels = [1] + res = await super().transfer_liquid( sources, targets, diff --git a/unilabos/resources/resource_tracker.py b/unilabos/resources/resource_tracker.py index 6a0755b5..2ba81b6d 100644 --- a/unilabos/resources/resource_tracker.py +++ b/unilabos/resources/resource_tracker.py @@ -489,7 +489,18 @@ class ResourceTreeSet(object): def resource_plr_inner( d: dict, parent_resource: Optional[ResourceDict], states: dict, uuids: list ) -> ResourceDictInstance: - current_uuid, parent_uuid, extra = uuids.pop(0) + if uuids: + current_uuid, parent_uuid, extra = uuids.pop(0) + else: + # serialize() 树比 res.children 树多出了节点(虚拟子节点等),兜底生成 UUID + current_uuid = str(uuid.uuid4()) + parent_uuid = parent_resource.get("uuid") if isinstance(parent_resource, dict) else ( + getattr(parent_resource, "uuid", None) if parent_resource is not None else None + ) + extra = {} + logger.warning( + f"from_plr_resources: UUID 列表耗尽,为节点 '{d.get('name', '?')}' 生成临时 UUID {current_uuid}" + ) raw_pos = ( {"x": d["location"]["x"], "y": d["location"]["y"], "z": d["location"]["z"]} diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index b9515af1..f7373d18 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -1739,13 +1739,19 @@ class BaseROS2DeviceNode(Node, Generic[T]): if arg_type == "unilabos.registry.placeholder_type:ResourceSlot": resource_data = function_args[arg_name] if isinstance(resource_data, dict) and "id" in resource_data: - try: - function_args[arg_name] = self._convert_resources_sync(resource_data["uuid"])[0] - except Exception as e: - self.lab_logger().error( - f"转换ResourceSlot参数 {arg_name} 失败: {e}\n{traceback.format_exc()}" - ) - raise JsonCommandInitError(f"ResourceSlot参数转换失败: {arg_name}") + uid = resource_data.get("uuid", "") + # 优先从本地追踪器直接取(避免服务端未同步导致的空返回) + local_fast = self.resource_tracker.uuid_to_resources.get(uid) if uid else None + if local_fast is not None: + function_args[arg_name] = local_fast + else: + try: + function_args[arg_name] = self._convert_resources_sync(uid)[0] + except Exception as e: + self.lab_logger().error( + f"转换ResourceSlot参数 {arg_name} 失败: {e}\n{traceback.format_exc()}" + ) + raise JsonCommandInitError(f"ResourceSlot参数转换失败: {arg_name}") # 处理 ResourceSlot 列表 elif isinstance(arg_type, tuple) and len(arg_type) == 2: @@ -1753,14 +1759,23 @@ class BaseROS2DeviceNode(Node, Generic[T]): if arg_type[0] == "list" and arg_type[1] == resource_slot_type: resource_list = function_args[arg_name] if isinstance(resource_list, list): - try: - uuids = [r["uuid"] for r in resource_list if isinstance(r, dict) and "id" in r] - function_args[arg_name] = self._convert_resources_sync(*uuids) if uuids else [] - except Exception as e: - self.lab_logger().error( - f"转换ResourceSlot列表参数 {arg_name} 失败: {e}\n{traceback.format_exc()}" - ) - raise JsonCommandInitError(f"ResourceSlot列表参数转换失败: {arg_name}") + uuids = [r["uuid"] for r in resource_list if isinstance(r, dict) and "id" in r] + # 先尝试本地追踪器批量取 + local_hits = [ + self.resource_tracker.uuid_to_resources[u] + for u in uuids + if u in self.resource_tracker.uuid_to_resources + ] + if len(local_hits) == len(uuids): + function_args[arg_name] = local_hits + else: + try: + function_args[arg_name] = self._convert_resources_sync(*uuids) if uuids else [] + except Exception as e: + self.lab_logger().error( + f"转换ResourceSlot列表参数 {arg_name} 失败: {e}\n{traceback.format_exc()}" + ) + raise JsonCommandInitError(f"ResourceSlot列表参数转换失败: {arg_name}") # todo: 默认反报送 return function(**function_args) @@ -1812,6 +1827,18 @@ class BaseROS2DeviceNode(Node, Generic[T]): # 转换为 PLR 资源 tree_set = ResourceTreeSet.from_raw_dict_list(raw_data) if not len(tree_set.trees): + # 服务端未找到时,尝试从本地追踪器兜底(create_resource 刚完成但服务端未及时同步) + local_hits = [ + self.resource_tracker.uuid_to_resources[uid] + for uid in uuids_list + if uid in self.resource_tracker.uuid_to_resources + ] + if local_hits: + self.lab_logger().warning( + f"资源查询服务端返回空树,已从本地追踪器找到 " + f"{len(local_hits)}/{len(uuids_list)} 个资源: {uuids_list}" + ) + return local_hits raise Exception(f"资源查询返回空树: {raw_data}") plr_resources = tree_set.to_plr_resources() diff --git a/unilabos/test/experiments/prcxi_9300_slim.json b/unilabos/test/experiments/prcxi_9300_slim.json index 3e7bd448..225a77fb 100644 --- a/unilabos/test/experiments/prcxi_9300_slim.json +++ b/unilabos/test/experiments/prcxi_9300_slim.json @@ -23,6 +23,7 @@ "port": 9999, "debug": false, "setup": true, + "matrix_id": "1ecb1b45-6aef-456b-bd68-8f538c4e5826", "timeout": 10, "simulator": false, "channel_num": 8 diff --git a/unilabos/test/experiments/prcxi_9320_slim.json b/unilabos/test/experiments/prcxi_9320_slim.json index 6b01e4ca..54aa9f4e 100644 --- a/unilabos/test/experiments/prcxi_9320_slim.json +++ b/unilabos/test/experiments/prcxi_9320_slim.json @@ -21,13 +21,14 @@ }, "host": "10.20.30.184", "port": 9999, - "debug": true, - "setup": true, + "debug": false, + "setup": false, "is_9320": true, "timeout": 10, - "matrix_id": "5de524d0-3f95-406c-86dd-f83626ebc7cb", - "simulator": true, - "channel_num": 2 + "matrix_id": "", + "simulator": false, + "channel_num": 2, + "step_mode": true }, "data": { "reset_ok": true @@ -49,8 +50,8 @@ "type": "deck", "class": "", "position": { - "x": 10, - "y": 10, + "x": 0, + "y": 0, "z": 0 }, "config": { @@ -66,426 +67,7 @@ }, "category": "deck", "barcode": null, - "preferred_pickup_location": null, - "sites": [ - { - "label": "T1", - "visible": true, - "occupied_by": null, - "position": { - "x": 0, - "y": 288, - "z": 0 - }, - "size": { - "width": 128.0, - "height": 86, - "depth": 0 - }, - "content_type": [ - "container", - "plate", - "tip_rack", - "plates", - "tip_racks", - "tube_rack", - "adaptor", - "plateadapter", - "module", - "trash" - ] - }, - { - "label": "T2", - "visible": true, - "occupied_by": null, - "position": { - "x": 138, - "y": 288, - "z": 0 - }, - "size": { - "width": 128.0, - "height": 86, - "depth": 0 - }, - "content_type": [ - "plate", - "tip_rack", - "plates", - "tip_racks", - "tube_rack", - "adaptor", - "plateadapter", - "module", - "trash" - ] - }, - { - "label": "T3", - "visible": true, - "occupied_by": null, - "position": { - "x": 276, - "y": 288, - "z": 0 - }, - "size": { - "width": 128.0, - "height": 86, - "depth": 0 - }, - "content_type": [ - "plate", - "tip_rack", - "plates", - "tip_racks", - "tube_rack", - "adaptor", - "plateadapter", - "module", - "trash" - ] - }, - { - "label": "T4", - "visible": true, - "occupied_by": null, - "position": { - "x": 414, - "y": 288, - "z": 0 - }, - "size": { - "width": 128.0, - "height": 86, - "depth": 0 - }, - "content_type": [ - "plate", - "tip_rack", - "plates", - "tip_racks", - "tube_rack", - "adaptor", - "plateadapter", - "module", - "trash" - ] - }, - { - "label": "T5", - "visible": true, - "occupied_by": null, - "position": { - "x": 0, - "y": 192, - "z": 0 - }, - "size": { - "width": 128.0, - "height": 86, - "depth": 0 - }, - "content_type": [ - "plate", - "tip_rack", - "plates", - "tip_racks", - "tube_rack", - "adaptor", - "plateadapter", - "module", - "trash" - ] - }, - { - "label": "T6", - "visible": true, - "occupied_by": null, - "position": { - "x": 138, - "y": 192, - "z": 0 - }, - "size": { - "width": 128.0, - "height": 86, - "depth": 0 - }, - "content_type": [ - "plate", - "tip_rack", - "plates", - "tip_racks", - "tube_rack", - "adaptor", - "plateadapter", - "module", - "trash" - ] - }, - { - "label": "T7", - "visible": true, - "occupied_by": null, - "position": { - "x": 276, - "y": 192, - "z": 0 - }, - "size": { - "width": 128.0, - "height": 86, - "depth": 0 - }, - "content_type": [ - "plate", - "tip_rack", - "plates", - "tip_racks", - "tube_rack", - "adaptor", - "plateadapter", - "module", - "trash" - ] - }, - { - "label": "T8", - "visible": true, - "occupied_by": null, - "position": { - "x": 414, - "y": 192, - "z": 0 - }, - "size": { - "width": 128.0, - "height": 86, - "depth": 0 - }, - "content_type": [ - "plate", - "tip_rack", - "plates", - "tip_racks", - "tube_rack", - "adaptor", - "plateadapter", - "module", - "trash" - ] - }, - { - "label": "T9", - "visible": true, - "occupied_by": null, - "position": { - "x": 0, - "y": 96, - "z": 0 - }, - "size": { - "width": 128.0, - "height": 86, - "depth": 0 - }, - "content_type": [ - "plate", - "tip_rack", - "plates", - "tip_racks", - "tube_rack", - "adaptor", - "plateadapter", - "module", - "trash" - ] - }, - { - "label": "T10", - "visible": true, - "occupied_by": null, - "position": { - "x": 138, - "y": 96, - "z": 0 - }, - "size": { - "width": 128.0, - "height": 86, - "depth": 0 - }, - "content_type": [ - "plate", - "tip_rack", - "plates", - "tip_racks", - "tube_rack", - "adaptor", - "plateadapter", - "module", - "trash" - ] - }, - { - "label": "T11", - "visible": true, - "occupied_by": null, - "position": { - "x": 276, - "y": 96, - "z": 0 - }, - "size": { - "width": 128.0, - "height": 86, - "depth": 0 - }, - "content_type": [ - "plate", - "tip_rack", - "plates", - "tip_racks", - "tube_rack", - "adaptor", - "plateadapter", - "module", - "trash" - ] - }, - { - "label": "T12", - "visible": true, - "occupied_by": null, - "position": { - "x": 414, - "y": 96, - "z": 0 - }, - "size": { - "width": 128.0, - "height": 86, - "depth": 0 - }, - "content_type": [ - "plate", - "tip_rack", - "plates", - "tip_racks", - "tube_rack", - "adaptor", - "plateadapter", - "module", - "trash" - ] - }, - { - "label": "T13", - "visible": true, - "occupied_by": null, - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "size": { - "width": 128.0, - "height": 86, - "depth": 0 - }, - "content_type": [ - "plate", - "tip_rack", - "plates", - "tip_racks", - "tube_rack", - "adaptor", - "plateadapter", - "module", - "trash" - ] - }, - { - "label": "T14", - "visible": true, - "occupied_by": null, - "position": { - "x": 138, - "y": 0, - "z": 0 - }, - "size": { - "width": 128.0, - "height": 86, - "depth": 0 - }, - "content_type": [ - "plate", - "tip_rack", - "plates", - "tip_racks", - "tube_rack", - "adaptor", - "plateadapter", - "module", - "trash" - ] - }, - { - "label": "T15", - "visible": true, - "occupied_by": null, - "position": { - "x": 276, - "y": 0, - "z": 0 - }, - "size": { - "width": 128.0, - "height": 86, - "depth": 0 - }, - "content_type": [ - "plate", - "tip_rack", - "plates", - "tip_racks", - "tube_rack", - "adaptor", - "plateadapter", - "module", - "trash" - ] - }, - { - "label": "T16", - "visible": true, - "occupied_by": null, - "position": { - "x": 414, - "y": 0, - "z": 0 - }, - "size": { - "width": 128.0, - "height": 86, - "depth": 0 - }, - "content_type": [ - "plate", - "tip_rack", - "plates", - "tip_racks", - "tube_rack", - "adaptor", - "plateadapter", - "module", - "trash" - ] - } - ] + "preferred_pickup_location": null }, "data": {} } diff --git a/unilabos/workflow/common.py b/unilabos/workflow/common.py index 5965a460..33d38e4b 100644 --- a/unilabos/workflow/common.py +++ b/unilabos/workflow/common.py @@ -624,7 +624,7 @@ def build_protocol_graph( workstation_name: str, action_resource_mapping: Optional[Dict[str, str]] = None, labware_defs: Optional[List[Dict[str, Any]]] = None, - preserve_tip_rack_incoming_class: bool = True, + preserve_tip_rack_incoming_class: bool = False, ) -> WorkflowGraph: """统一的协议图构建函数,根据设备类型自动选择构建逻辑 diff --git a/unilabos/workflow/convert_from_json.py b/unilabos/workflow/convert_from_json.py index cd88552b..56b8c160 100644 --- a/unilabos/workflow/convert_from_json.py +++ b/unilabos/workflow/convert_from_json.py @@ -210,7 +210,7 @@ def convert_from_json( data: Union[str, PathLike, Dict[str, Any]], workstation_name: str = DEFAULT_WORKSTATION, validate: bool = True, - preserve_tip_rack_incoming_class: bool = True, + preserve_tip_rack_incoming_class: bool = False, ) -> WorkflowGraph: """ 从 JSON 数据或文件转换为 WorkflowGraph @@ -295,7 +295,7 @@ def convert_from_json( def convert_json_to_node_link( data: Union[str, PathLike, Dict[str, Any]], workstation_name: str = DEFAULT_WORKSTATION, - preserve_tip_rack_incoming_class: bool = True, + preserve_tip_rack_incoming_class: bool = False, ) -> Dict[str, Any]: """ 将 JSON 数据转换为 node-link 格式的字典 diff --git a/unilabos/workflow/legacy/convert_from_json_legacy.py b/unilabos/workflow/legacy/convert_from_json_legacy.py index 3d830d94..54424a84 100644 --- a/unilabos/workflow/legacy/convert_from_json_legacy.py +++ b/unilabos/workflow/legacy/convert_from_json_legacy.py @@ -234,7 +234,7 @@ def convert_from_json( data: Union[str, PathLike, Dict[str, Any]], workstation_name: str = "PRCXi", validate: bool = True, - preserve_tip_rack_incoming_class: bool = True, + preserve_tip_rack_incoming_class: bool = False, ) -> WorkflowGraph: """ 从 JSON 数据或文件转换为 WorkflowGraph