From e5015cd5e00bb8f86c331d4e71aaa5606e6ece26 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 2 Mar 2026 15:52:28 +0800 Subject: [PATCH 01/12] fix size change --- unilabos/resources/resource_tracker.py | 9 +++++++++ unilabos/ros/nodes/base_device_node.py | 10 +++++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/unilabos/resources/resource_tracker.py b/unilabos/resources/resource_tracker.py index e042ef80..629375f3 100644 --- a/unilabos/resources/resource_tracker.py +++ b/unilabos/resources/resource_tracker.py @@ -411,6 +411,15 @@ class ResourceTreeSet(object): "tip_spot": "tip_spot", "tube": "tube", "bottle_carrier": "bottle_carrier", + "material_hole": "material_hole", + "container": "container", + "material_plate": "material_plate", + "electrode_sheet": "electrode_sheet", + "warehouse": "warehouse", + "magazine_holder": "magazine_holder", + "resource_group": "resource_group", + "trash": "trash", + "plate_adapter": "plate_adapter", } if source in replace_info: return replace_info[source] diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index eddb57a2..7ae16961 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -924,6 +924,14 @@ class BaseROS2DeviceNode(Node, Generic[T]): parent_appended = True # 加载状态 + # noinspection PyProtectedMember + original_instance._size_x = plr_resource._size_x + # noinspection PyProtectedMember + original_instance._size_y = plr_resource._size_y + # noinspection PyProtectedMember + original_instance._size_z = plr_resource._size_z + # noinspection PyProtectedMember + original_instance._local_size_z = plr_resource._local_size_z original_instance.location = plr_resource.location original_instance.rotation = plr_resource.rotation original_instance.barcode = plr_resource.barcode @@ -984,7 +992,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): ].call_async( r ) # type: ignore - self.lab_logger().info(f"确认资源云端 Add 结果: {response.response}") + self.lab_logger().trace(f"确认资源云端 Add 结果: {response.response}") results.append(result) elif action == "update": if tree_set is None: From a54e7c0f230567ab5993f1323ba28a6fae050ab3 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Mon, 2 Mar 2026 18:29:25 +0800 Subject: [PATCH 02/12] new workflow & prcxi slot removal --- .../devices/liquid_handling/prcxi/prcxi.py | 117 +- unilabos/resources/graphio.py | 2 +- unilabos/resources/resource_tracker.py | 2 +- unilabos/ros/nodes/base_device_node.py | 22 +- unilabos/ros/nodes/presets/host_node.py | 2 +- .../test/experiments/prcxi_9320_slim.json | 1220 ++++++----------- unilabos/workflow/common.py | 6 +- 7 files changed, 552 insertions(+), 819 deletions(-) diff --git a/unilabos/devices/liquid_handling/prcxi/prcxi.py b/unilabos/devices/liquid_handling/prcxi/prcxi.py index 3f33c960..c07a6de2 100644 --- a/unilabos/devices/liquid_handling/prcxi/prcxi.py +++ b/unilabos/devices/liquid_handling/prcxi/prcxi.py @@ -55,6 +55,7 @@ from unilabos.devices.liquid_handling.liquid_handler_abstract import ( TransferLiquidReturn, ) from unilabos.registry.placeholder_type import ResourceSlot +from unilabos.resources.resource_tracker import ResourceTreeSet from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode @@ -90,20 +91,103 @@ class PRCXI9300Deck(Deck): 该类定义了 PRCXI 9300 的工作台布局和槽位信息。 """ - def __init__(self, name: str, size_x: float, size_y: float, size_z: float, **kwargs): + # T1-T16 默认位置 (4列×4行) + _DEFAULT_SITE_POSITIONS = [ + (0, 288, 0), (138, 288, 0), (276, 288, 0), (414, 288, 0), # T1-T4 + (0, 192, 0), (138, 192, 0), (276, 192, 0), (414, 192, 0), # T5-T8 + (0, 96, 0), (138, 96, 0), (276, 96, 0), (414, 96, 0), # T9-T12 + (0, 0, 0), (138, 0, 0), (276, 0, 0), (414, 0, 0), # T13-T16 + ] + _DEFAULT_SITE_SIZE = {"width": 128.0, "height": 86, "depth": 0} + _DEFAULT_CONTENT_TYPE = ["plate", "tip_rack", "plates", "tip_racks", "tube_rack"] + + def __init__(self, name: str, size_x: float, size_y: float, size_z: float, + sites: Optional[List[Dict[str, Any]]] = None, **kwargs): super().__init__(size_x, size_y, size_z, name) - self.slots = [None] * 16 # PRCXI 9300/9320 最大有 16 个槽位 - self.slot_locations = [Coordinate(0, 0, 0)] * 16 + if sites is not None: + self.sites: List[Dict[str, Any]] = [dict(s) for s in sites] + else: + self.sites = [] + for i, (x, y, z) in enumerate(self._DEFAULT_SITE_POSITIONS): + self.sites.append({ + "label": f"T{i + 1}", + "visible": True, + "position": {"x": x, "y": y, "z": z}, + "size": dict(self._DEFAULT_SITE_SIZE), + "content_type": list(self._DEFAULT_CONTENT_TYPE), + }) + # _ordering: label -> None, 用于外部通过 list(keys()).index(site) 将 Tn 转换为 spot index + self._ordering = collections.OrderedDict( + (site["label"], None) for site in self.sites + ) + + def _get_site_location(self, idx: int) -> Coordinate: + pos = self.sites[idx]["position"] + return Coordinate(pos["x"], pos["y"], pos["z"]) + + def _get_site_resource(self, idx: int) -> Optional[Resource]: + site_loc = self._get_site_location(idx) + for child in self.children: + if child.location == site_loc: + return child + return None + + def assign_child_resource( + self, + resource: Resource, + location: Optional[Coordinate] = None, + reassign: bool = True, + spot: Optional[int] = None, + ): + idx = spot + if spot is not None: + idx = spot + else: + for i, site in enumerate(self.sites): + site_loc = self._get_site_location(i) + if site.get("label") == resource.name: + idx = i + break + if location is not None and site_loc == location: + idx = i + break + + if idx is None: + for i in range(len(self.sites)): + if self._get_site_resource(i) is None: + idx = i + break + + if idx is None: + raise ValueError(f"No available site on deck '{self.name}' for resource '{resource.name}'") + + if not reassign and self._get_site_resource(idx) is not None: + raise ValueError(f"Site {idx} ('{self.sites[idx]['label']}') is already occupied") + + loc = self._get_site_location(idx) + super().assign_child_resource(resource, location=loc, reassign=reassign) def assign_child_at_slot(self, resource: Resource, slot: int, reassign: bool = False) -> None: - if self.slots[slot - 1] is not None and not reassign: - raise ValueError(f"Spot {slot} is already occupied") + self.assign_child_resource(resource, spot=slot - 1, reassign=reassign) - self.slots[slot - 1] = resource - super().assign_child_resource(resource, location=self.slot_locations[slot - 1]) + def serialize(self) -> dict: + data = super().serialize() + sites_out = [] + for i, site in enumerate(self.sites): + occupied = self._get_site_resource(i) + sites_out.append({ + "label": site["label"], + "visible": site.get("visible", True), + "occupied_by": occupied.name if occupied is not None else None, + "position": site["position"], + "size": site["size"], + "content_type": site["content_type"], + }) + data["sites"] = sites_out + return data -class PRCXI9300Container(Plate): +class PRCXI9300Container(Container): """PRCXI 9300 的专用 Container 类,继承自 Plate,用于槽位定位和未知模块。 该类定义了 PRCXI 9300 的工作台布局和槽位信息。 @@ -116,11 +200,10 @@ class PRCXI9300Container(Plate): size_y: float, size_z: float, category: str, - ordering: collections.OrderedDict, model: Optional[str] = None, **kwargs, ): - super().__init__(name, size_x, size_y, size_z, category=category, ordering=ordering, model=model) + super().__init__(name, size_x, size_y, size_z, category=category, model=model) self._unilabos_state = {} def load_state(self, state: Dict[str, Any]) -> None: @@ -567,14 +650,14 @@ class PRCXI9300Handler(LiquidHandlerAbstract): tablets_info = [] count = 0 for child in deck.children: - if child.children: - if "Material" in child.children[0]._unilabos_state: - number = int(child.name.replace("T", "")) - tablets_info.append( - WorkTablets( - Number=number, Code=f"T{number}", Material=child.children[0]._unilabos_state["Material"] - ) + # 如果放其他类型的物料,是不可以的 + if hasattr(child, "_unilabos_state") and "Material" in child._unilabos_state: + number = int(child.name.replace("T", "")) + tablets_info.append( + WorkTablets( + Number=number, Code=f"T{number}", Material=child._unilabos_state["Material"] ) + ) if is_9320: print("当前设备是9320") # 始终初始化 step_mode 属性 diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index 1b8f97f1..38f96968 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -76,7 +76,7 @@ def canonicalize_nodes_data( if sample_id: logger.error(f"{node}的sample_id参数已弃用,sample_id: {sample_id}") for k in list(node.keys()): - if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data", "children", "pose"]: + if k not in ["id", "uuid", "name", "description", "schema", "model", "icon", "parent_uuid", "parent", "type", "class", "position", "config", "data", "children", "pose", "extra"]: v = node.pop(k) node["config"][k] = v if outer_host_node_id is not None: diff --git a/unilabos/resources/resource_tracker.py b/unilabos/resources/resource_tracker.py index 629375f3..a3e3d3f2 100644 --- a/unilabos/resources/resource_tracker.py +++ b/unilabos/resources/resource_tracker.py @@ -616,7 +616,7 @@ class ResourceTreeSet(object): plr_resources.append(plr_resource) except Exception as e: - logger.error(f"转换 PLR 资源失败: {e}") + logger.error(f"转换 PLR 资源失败: {e} {str(plr_dict)[:1000]}") import traceback logger.error(f"堆栈: {traceback.format_exc()}") diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index 7ae16961..6ff8cc57 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -915,8 +915,24 @@ class BaseROS2DeviceNode(Node, Generic[T]): else [] ) if target_site is not None and sites is not None and site_names is not None: - site_index = sites.index(original_instance) - site_name = site_names[site_index] + site_index = None + try: + # sites 可能是 Resource 列表或 dict 列表 (如 PRCXI9300Deck) + # 只有itemized_carrier在使用,准备弃用 + site_index = sites.index(original_instance) + except ValueError: + # dict 类型的 sites: 通过name匹配 + for idx, site in enumerate(sites): + if original_instance.name == site["occupied_by"]: + site_index = idx + break + elif (original_instance.location.x == site["position"]["x"] and original_instance.location.y == site["position"]["y"] and original_instance.location.z == site["position"]["z"]): + site_index = idx + break + if site_index is None: + site_name = None + else: + site_name = site_names[site_index] if site_name != target_site: parent = self.transfer_to_new_resource(original_instance, tree, additional_add_params) if parent is not None: @@ -1018,7 +1034,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): ].call_async( r ) # type: ignore - self.lab_logger().info(f"确认资源云端 Update 结果: {response.response}") + self.lab_logger().trace(f"确认资源云端 Update 结果: {response.response}") results.append(result) elif action == "remove": result = _handle_remove(resources_uuid) diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 30c5d414..4a868523 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -1195,7 +1195,7 @@ class HostNode(BaseROS2DeviceNode): self.lab_logger().info(f"[Host Node-Resource] UUID映射: {len(uuid_mapping)} 个节点") # 还需要加入到资源图中,暂不实现,考虑资源图新的获取方式 response.response = json.dumps(uuid_mapping) - self.lab_logger().info(f"[Host Node-Resource] Resource tree add completed, success: {success}") + self.lab_logger().info(f"[Host Node-Resource] Resource tree update completed, success: {success}") async def _resource_tree_update_callback(self, request: SerialCommand_Request, response: SerialCommand_Response): """ diff --git a/unilabos/test/experiments/prcxi_9320_slim.json b/unilabos/test/experiments/prcxi_9320_slim.json index 85bc90cf..a3be4506 100644 --- a/unilabos/test/experiments/prcxi_9320_slim.json +++ b/unilabos/test/experiments/prcxi_9320_slim.json @@ -1,795 +1,429 @@ { - "nodes": [ - { - "id": "PRCXI", - "name": "PRCXI", - "type": "device", - "class": "liquid_handler.prcxi", - "parent": "", - "pose": { - "size": { - "width": 562, - "height": 394, - "depth": 0 + "nodes": [ + { + "id": "PRCXI", + "name": "PRCXI", + "type": "device", + "class": "liquid_handler.prcxi", + "parent": "", + "pose": { + "size": { + "width": 562, + "height": 394, + "depth": 0 + } + }, + "config": { + "axis": "Left", + "deck": { + "_resource_type": "unilabos.devices.liquid_handling.prcxi.prcxi:PRCXI9300Deck", + "_resource_child_name": "PRCXI_Deck" + }, + "host": "10.20.30.184", + "port": 9999, + "debug": true, + "setup": true, + "is_9320": true, + "timeout": 10, + "matrix_id": "5de524d0-3f95-406c-86dd-f83626ebc7cb", + "simulator": true, + "channel_num": 2 + }, + "data": { + "reset_ok": true + }, + "schema": {}, + "description": "", + "model": null, + "position": { + "x": 0, + "y": 240, + "z": 0 + } + }, + { + "id": "PRCXI_Deck", + "name": "PRCXI_Deck", + "children": [], + "parent": "PRCXI", + "type": "deck", + "class": "", + "position": { + "x": 10, + "y": 10, + "z": 0 + }, + "config": { + "type": "PRCXI9300Deck", + "size_x": 542, + "size_y": 374, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "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": [ + "plate", + "tip_rack", + "plates", + "tip_racks", + "tube_rack" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + } + ] + }, + "data": {} } - }, - "config": { - "axis": "Left", - "deck": { - "_resource_type": "unilabos.devices.liquid_handling.prcxi.prcxi:PRCXI9300Deck", - "_resource_child_name": "PRCXI_Deck" - }, - "host": "10.20.30.184", - "port": 9999, - "debug": true, - "setup": true, - "is_9320": true, - "timeout": 10, - "matrix_id": "5de524d0-3f95-406c-86dd-f83626ebc7cb", - "simulator": true, - "channel_num": 2 - }, - "data": { - "reset_ok": true - }, - "schema": {}, - "description": "", - "model": null, - "position": { - "x": 0, - "y": 240, - "z": 0 - } - }, - { - "id": "PRCXI_Deck", - "name": "PRCXI_Deck", - - "children": [], - "parent": "PRCXI", - "type": "deck", - "class": "", - "position": { - "x": 10, - "y": 10, - "z": 0 - }, - "config": { - "type": "PRCXI9300Deck", - "size_x": 542, - "size_y": 374, - "size_z": 0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "deck", - "barcode": null - }, - "data": {} - }, - { - "id": "T1", - "name": "T1", - "children": [], - "parent": "PRCXI_Deck", - "type": "plate", - "class": "", - "position": { - "x": 0, - "y": 288, - "z": 0 - }, - "config": { - "type": "PRCXI9300Container", - "size_x": 127, - "size_y": 85.5, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "plate", - "model": null, - "barcode": null, - "ordering": {}, - "sites": [ - { - "label": "T1", - "visible": true, - "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" - ] - } - ] - }, - "data": {} - }, - { - "id": "T2", - "name": "T2", - "children": [], - "parent": "PRCXI_Deck", - "type": "plate", - "class": "", - "position": { - "x": 138, - "y": 288, - "z": 0 - }, - "config": { - "type": "PRCXI9300Container", - "size_x": 127, - "size_y": 85.5, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "plate", - "model": null, - "barcode": null, - "ordering": {}, - "sites": [ - { - "label": "T2", - "visible": true, - "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" - ] - } - ] - }, - "data": {} - }, - { - "id": "T3", - "name": "T3", - "children": [], - "parent": "PRCXI_Deck", - "type": "plate", - "class": "", - "position": { - "x": 276, - "y": 288, - "z": 0 - }, - "config": { - "type": "PRCXI9300Container", - "size_x": 127, - "size_y": 85.5, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "plate", - "model": null, - "barcode": null, - "ordering": {}, - "sites": [ - { - "label": "T3", - "visible": true, - "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" - ] - } - ] - }, - "data": {} - }, - { - "id": "T4", - "name": "T4", - "children": [], - "parent": "PRCXI_Deck", - "type": "plate", - "class": "", - "position": { - "x": 414, - "y": 288, - "z": 0 - }, - "config": { - "type": "PRCXI9300Container", - "size_x": 127, - "size_y": 85.5, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "plate", - "model": null, - "barcode": null, - "ordering": {}, - "sites": [ - { - "label": "T4", - "visible": true, - "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" - ] - } - ] - }, - "data": {} - }, - { - "id": "T5", - "name": "T5", - "children": [], - "parent": "PRCXI_Deck", - "type": "plate", - "class": "", - "position": { - "x": 0, - "y": 192, - "z": 0 - }, - "config": { - "type": "PRCXI9300Container", - "size_x": 127, - "size_y": 85.5, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "plate", - "model": null, - "barcode": null, - "ordering": {}, - "sites": [ - { - "label": "T5", - "visible": true, - "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" - ] - } - ] - }, - "data": {} - }, - { - "id": "T6", - "name": "T6", - "children": [], - "parent": "PRCXI_Deck", - "type": "plate", - "class": "", - "position": { - "x": 138, - "y": 192, - "z": 0 - }, - "config": { - "type": "PRCXI9300Container", - "size_x": 127, - "size_y": 85.5, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "plate", - "model": null, - "barcode": null, - "ordering": {}, - "sites": [ - { - "label": "T6", - "visible": true, - "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" - ] - } - ] - }, - "data": {} - }, - { - "id": "T7", - "name": "T7", - "children": [], - "parent": "PRCXI_Deck", - "type": "plate", - "class": "", - "position": { - "x": 276, - "y": 192, - "z": 0 - }, - "config": { - "type": "PRCXI9300Container", - "size_x": 127, - "size_y": 85.5, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "plate", - "model": null, - "barcode": null, - "ordering": {}, - "sites": [ - { - "label": "T7", - "visible": true, - "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" - ] - } - ] - }, - "data": {} - }, - { - "id": "T8", - "name": "T8", - "children": [], - "parent": "PRCXI_Deck", - "type": "plate", - "class": "", - "position": { - "x": 414, - "y": 192, - "z": 0 - }, - "config": { - "type": "PRCXI9300Container", - "size_x": 127, - "size_y": 85.5, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "plate", - "model": null, - "barcode": null, - "ordering": {}, - "sites": [ - { - "label": "T8", - "visible": true, - "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" - ] - } - ] - }, - "data": {} - }, - { - "id": "T9", - "name": "T9", - "children": [], - "parent": "PRCXI_Deck", - "type": "plate", - "class": "", - "position": { - "x": 0, - "y": 96, - "z": 0 - }, - "config": { - "type": "PRCXI9300Container", - "size_x": 127, - "size_y": 85.5, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "plate", - "model": null, - "barcode": null, - "ordering": {}, - "sites": [ - { - "label": "T9", - "visible": true, - "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" - ] - } - ] - }, - "data": {} - }, - { - "id": "T10", - "name": "T10", - "children": [], - "parent": "PRCXI_Deck", - "type": "plate", - "class": "", - "position": { - "x": 138, - "y": 96, - "z": 0 - }, - "config": { - "type": "PRCXI9300Container", - "size_x": 127, - "size_y": 85.5, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "plate", - "model": null, - "barcode": null, - "ordering": {}, - "sites": [ - { - "label": "T10", - "visible": true, - "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" - ] - } - ] - }, - "data": {} - }, - { - "id": "T11", - "name": "T11", - "children": [], - "parent": "PRCXI_Deck", - "type": "plate", - "class": "", - "position": { - "x": 276, - "y": 96, - "z": 0 - }, - "config": { - "type": "PRCXI9300Container", - "size_x": 127, - "size_y": 85.5, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "plate", - "model": null, - "barcode": null, - "ordering": {}, - "sites": [ - { - "label": "T11", - "visible": true, - "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" - ] - } - ] - }, - "data": {} - }, - { - "id": "T12", - "name": "T12", - "children": [], - "parent": "PRCXI_Deck", - "type": "plate", - "class": "", - "position": { - "x": 414, - "y": 96, - "z": 0 - }, - "config": { - "type": "PRCXI9300Container", - "size_x": 127, - "size_y": 85.5, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "plate", - "model": null, - "barcode": null, - "ordering": {}, - "sites": [ - { - "label": "T12", - "visible": true, - "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" - ] - } - ] - }, - "data": {} - }, - { - "id": "T13", - "name": "T13", - "children": [], - "parent": "PRCXI_Deck", - "type": "plate", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "PRCXI9300Container", - "size_x": 127, - "size_y": 85.5, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "plate", - "model": null, - "barcode": null, - "ordering": {}, - "sites": [ - { - "label": "T13", - "visible": true, - "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" - ] - } - ] - }, - "data": {} - }, - { - "id": "T14", - "name": "T14", - "children": [], - "parent": "PRCXI_Deck", - "type": "plate", - "class": "", - "position": { - "x": 138, - "y": 0, - "z": 0 - }, - "config": { - "type": "PRCXI9300Container", - "size_x": 127, - "size_y": 85.5, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "plate", - "model": null, - "barcode": null, - "ordering": {}, - "sites": [ - { - "label": "T14", - "visible": true, - "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" - ] - } - ] - }, - "data": {} - }, - { - "id": "T15", - "name": "T15", - "children": [], - "parent": "PRCXI_Deck", - "type": "plate", - "class": "", - "position": { - "x": 276, - "y": 0, - "z": 0 - }, - "config": { - "type": "PRCXI9300Container", - "size_x": 127, - "size_y": 85.5, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "plate", - "model": null, - "barcode": null, - "ordering": {}, - "sites": [ - { - "label": "T15", - "visible": true, - "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" - ] - } - ] - }, - "data": {} - }, - { - "id": "T16", - "name": "T16", - "children": [], - "parent": "PRCXI_Deck", - "type": "plate", - "class": "", - "position": { - "x": 414, - "y": 0, - "z": 0 - }, - "config": { - "type": "PRCXI9300Container", - "size_x": 127, - "size_y": 85.5, - "size_z": 10, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "plate", - "model": null, - "barcode": null, - "ordering": {}, - "sites": [ - { - "label": "T16", - "visible": true, - "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" - ] - } - ] - }, - "data": {} - } - ], - "edges": [] -} + ], + "edges": [] +} \ No newline at end of file diff --git a/unilabos/workflow/common.py b/unilabos/workflow/common.py index 3a1fee22..3e2fec92 100644 --- a/unilabos/workflow/common.py +++ b/unilabos/workflow/common.py @@ -26,7 +26,7 @@ res_id: plate_slot_{slot} device_id: /PRCXI class_name: PRCXI_BioER_96_wellplate - parent: /PRCXI/PRCXI_Deck/T{slot} + parent: /PRCXI/PRCXI_Deck slot_on_deck: "{slot}" - 输出端口: labware(用于连接 set_liquid_from_plate) - 控制流: create_resource 之间通过 ready 端口串联 @@ -122,7 +122,7 @@ NODE_TYPE_DEFAULT = "ILab" # 所有节点的默认类型 # create_resource 节点默认参数 CREATE_RESOURCE_DEFAULTS = { "device_id": "/PRCXI", - "parent_template": "/PRCXI/PRCXI_Deck/T{slot}", # {slot} 会被替换为实际的 slot 值 + "parent_template": "/PRCXI/PRCXI_Deck", "class_name": "PRCXI_BioER_96_wellplate", } @@ -424,7 +424,7 @@ def build_protocol_graph( "res_id": res_id, "device_id": CREATE_RESOURCE_DEFAULTS["device_id"], "class_name": lw_type, - "parent": CREATE_RESOURCE_DEFAULTS["parent_template"].format(slot=slot), + "parent": CREATE_RESOURCE_DEFAULTS["parent_template"], "bind_locations": {"x": 0.0, "y": 0.0, "z": 0.0}, "slot_on_deck": slot, }, From 3f160c204958c129eda52dbde519cec057831feb Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:40:02 +0800 Subject: [PATCH 03/12] =?UTF-8?q?=E6=9B=B4=E6=96=B0prcxi=20deck=20&=20?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20unilabos=5Fresource=5Fslot?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unilabos/devices/liquid_handling/prcxi/prcxi.py | 10 +++++----- unilabos/registry/registry.py | 3 ++- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/unilabos/devices/liquid_handling/prcxi/prcxi.py b/unilabos/devices/liquid_handling/prcxi/prcxi.py index c07a6de2..f34583fe 100644 --- a/unilabos/devices/liquid_handling/prcxi/prcxi.py +++ b/unilabos/devices/liquid_handling/prcxi/prcxi.py @@ -93,13 +93,13 @@ class PRCXI9300Deck(Deck): # T1-T16 默认位置 (4列×4行) _DEFAULT_SITE_POSITIONS = [ - (0, 288, 0), (138, 288, 0), (276, 288, 0), (414, 288, 0), # T1-T4 - (0, 192, 0), (138, 192, 0), (276, 192, 0), (414, 192, 0), # T5-T8 - (0, 96, 0), (138, 96, 0), (276, 96, 0), (414, 96, 0), # T9-T12 - (0, 0, 0), (138, 0, 0), (276, 0, 0), (414, 0, 0), # T13-T16 + (0, 0, 0), (138, 0, 0), (276, 0, 0), (414, 0, 0), # T1-T4 + (0, 96, 0), (138, 96, 0), (276, 96, 0), (414, 96, 0), # T5-T8 + (0, 192, 0), (138, 192, 0), (276, 192, 0), (414, 192, 0), # T9-T12 + (0, 288, 0), (138, 288, 0), (276, 288, 0), (414, 288, 0), # T13-T16 ] _DEFAULT_SITE_SIZE = {"width": 128.0, "height": 86, "depth": 0} - _DEFAULT_CONTENT_TYPE = ["plate", "tip_rack", "plates", "tip_racks", "tube_rack"] + _DEFAULT_CONTENT_TYPE = ["plate", "tip_rack", "plates", "tip_racks", "tube_rack", "adaptor"] def __init__(self, name: str, size_x: float, size_y: float, size_z: float, sites: Optional[List[Dict[str, Any]]] = None, **kwargs): diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py index 844d4cf8..02d80cca 100644 --- a/unilabos/registry/registry.py +++ b/unilabos/registry/registry.py @@ -175,7 +175,8 @@ class Registry: "res_id": "unilabos_resources", # 将当前实验室的全部物料id作为下拉框可选择 "device_id": "unilabos_devices", # 将当前实验室的全部设备id作为下拉框可选择 "parent": "unilabos_nodes", # 将当前实验室的设备/物料作为下拉框可选择 - "class_name": "unilabos_class", + "class_name": "unilabos_class", # 当前实验室物料的class name + "slot_on_deck": "unilabos_resource_slot:parent", # 勾选的parent的config中的sites的name,展示name,参数对应slot(index) }, }, "test_latency": { From 7f1cc3b2a5ffe3750124f0bbba253df4598bc326 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:43:41 +0800 Subject: [PATCH 04/12] update materials --- .../test/experiments/prcxi_9320_slim.json | 438 +++++++++--------- 1 file changed, 227 insertions(+), 211 deletions(-) diff --git a/unilabos/test/experiments/prcxi_9320_slim.json b/unilabos/test/experiments/prcxi_9320_slim.json index a3be4506..e97aa3d9 100644 --- a/unilabos/test/experiments/prcxi_9320_slim.json +++ b/unilabos/test/experiments/prcxi_9320_slim.json @@ -74,7 +74,7 @@ "occupied_by": null, "position": { "x": 0, - "y": 288, + "y": 0, "z": 0 }, "size": { @@ -87,7 +87,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "adaptor" ] }, { @@ -96,7 +97,7 @@ "occupied_by": null, "position": { "x": 138, - "y": 288, + "y": 0, "z": 0 }, "size": { @@ -109,7 +110,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "adaptor" ] }, { @@ -118,7 +120,7 @@ "occupied_by": null, "position": { "x": 276, - "y": 288, + "y": 0, "z": 0 }, "size": { @@ -131,7 +133,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "adaptor" ] }, { @@ -140,6 +143,213 @@ "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" + ] + }, + { + "label": "T5", + "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" + ] + }, + { + "label": "T6", + "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" + ] + }, + { + "label": "T7", + "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" + ] + }, + { + "label": "T8", + "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" + ] + }, + { + "label": "T9", + "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" + ] + }, + { + "label": "T10", + "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" + ] + }, + { + "label": "T11", + "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" + ] + }, + { + "label": "T12", + "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" + ] + }, + { + "label": "T13", + "visible": true, + "occupied_by": null, + "position": { + "x": 0, "y": 288, "z": 0 }, @@ -153,205 +363,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" - ] - }, - { - "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" - ] - }, - { - "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" - ] - }, - { - "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" - ] - }, - { - "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" - ] - }, - { - "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" - ] - }, - { - "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" - ] - }, - { - "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" - ] - }, - { - "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" - ] - }, - { - "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" + "tube_rack", + "adaptor" ] }, { @@ -360,7 +373,7 @@ "occupied_by": null, "position": { "x": 138, - "y": 0, + "y": 288, "z": 0 }, "size": { @@ -373,7 +386,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "adaptor" ] }, { @@ -382,7 +396,7 @@ "occupied_by": null, "position": { "x": 276, - "y": 0, + "y": 288, "z": 0 }, "size": { @@ -395,7 +409,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "adaptor" ] }, { @@ -404,7 +419,7 @@ "occupied_by": null, "position": { "x": 414, - "y": 0, + "y": 288, "z": 0 }, "size": { @@ -417,7 +432,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "adaptor" ] } ] From b40c08714305927ce150fe325e47d73eac473ba5 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:13:32 +0800 Subject: [PATCH 05/12] fix container volume --- unilabos/app/main.py | 3 ++- unilabos/resources/container.py | 11 +++-------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/unilabos/app/main.py b/unilabos/app/main.py index c652757c..93751262 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -1,6 +1,7 @@ import argparse import asyncio import os +import platform import shutil import signal import sys @@ -358,7 +359,7 @@ def main(): if BasicConfig.test_mode: print_status("启用测试模式:所有动作将模拟执行,不调用真实硬件", "warning") BasicConfig.communication_protocol = "websocket" - machine_name = os.popen("hostname").read().strip() + machine_name = platform.node() machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name]) BasicConfig.machine_name = machine_name BasicConfig.vis_2d_enable = args_dict["2d_vis"] diff --git a/unilabos/resources/container.py b/unilabos/resources/container.py index fe19bacf..ed3871d3 100644 --- a/unilabos/resources/container.py +++ b/unilabos/resources/container.py @@ -1,10 +1,6 @@ -import json from typing import Dict, Any from pylabrobot.resources import Container -from unilabos_msgs.msg import Resource - -from unilabos.ros.msgs.message_converter import convert_from_ros_msg class RegularContainer(Container): @@ -16,12 +12,12 @@ class RegularContainer(Container): kwargs["size_y"] = 0 if "size_z" not in kwargs: kwargs["size_z"] = 0 + self.kwargs = kwargs - self.state = {} super().__init__(*args, category="container", **kwargs) def load_state(self, state: Dict[str, Any]): - self.state = state + super().load_state(state) def get_regular_container(name="container"): @@ -29,7 +25,6 @@ def get_regular_container(name="container"): r.category = "container" return r -# # class RegularContainer(object): # # 第一个参数必须是id传入 # # noinspection PyShadowingBuiltins @@ -89,4 +84,4 @@ def get_regular_container(name="container"): # return to_dict # # def __str__(self): -# return f"{self.id}" \ No newline at end of file +# return f"{self.id}" From 5c047beb83b0ab849deab73c424d2b61c2054dde Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Tue, 3 Mar 2026 18:03:53 +0800 Subject: [PATCH 06/12] support container as example add z index (cherry picked from commit 145fcaae65c6e729570c2a0507d1a0f6a43252ce) --- unilabos/resources/resource_tracker.py | 12 ++++++++++++ unilabos/test/experiments/prcxi_9320_slim.json | 1 + 2 files changed, 13 insertions(+) diff --git a/unilabos/resources/resource_tracker.py b/unilabos/resources/resource_tracker.py index a3e3d3f2..8ade8e1f 100644 --- a/unilabos/resources/resource_tracker.py +++ b/unilabos/resources/resource_tracker.py @@ -16,6 +16,7 @@ if TYPE_CHECKING: EXTRA_CLASS = "unilabos_resource_class" +FRONTEND_POSE_EXTRA = "unilabos_frontend_pose_extra" EXTRA_SAMPLE_UUID = "sample_uuid" EXTRA_UNILABOS_SAMPLE_UUID = "unilabos_sample_uuid" @@ -74,6 +75,14 @@ class ResourceDictPositionObject(BaseModel): z: float = Field(description="Z coordinate", default=0.0) +class ResourceDictPoseExtraObjectType(BaseModel): + z_index: int + + +class ResourceDictPoseExtraObject(BaseModel): + z_index: Optional[int] = Field(alias="zIndex", default=None) + + class ResourceDictPositionType(TypedDict): size: ResourceDictPositionSizeType scale: ResourceDictPositionScaleType @@ -100,6 +109,7 @@ class ResourceDictPosition(BaseModel): cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"] = Field( description="Cross section type", default="rectangle" ) + extra: Optional[ResourceDictPoseExtraObject] = Field(description="Extra data", default=None) class ResourceDictType(TypedDict): @@ -463,6 +473,7 @@ class ResourceTreeSet(object): "position3d": raw_pos, "rotation": d["rotation"], "cross_section_type": d.get("cross_section_type", "rectangle"), + "extra": extra.get(FRONTEND_POSE_EXTRA) } # 先构建当前节点的字典(不包含children) @@ -548,6 +559,7 @@ class ResourceTreeSet(object): name_to_uuid[node.res_content.name] = node.res_content.uuid all_states[node.res_content.name] = node.res_content.data name_to_extra[node.res_content.name] = node.res_content.extra + name_to_extra[node.res_content.name][FRONTEND_POSE_EXTRA] = node.res_content.pose.extra name_to_extra[node.res_content.name][EXTRA_CLASS] = node.res_content.klass for child in node.children: collect_node_data(child, name_to_uuid, all_states, name_to_extra) diff --git a/unilabos/test/experiments/prcxi_9320_slim.json b/unilabos/test/experiments/prcxi_9320_slim.json index e97aa3d9..2aaee6a7 100644 --- a/unilabos/test/experiments/prcxi_9320_slim.json +++ b/unilabos/test/experiments/prcxi_9320_slim.json @@ -83,6 +83,7 @@ "depth": 0 }, "content_type": [ + "container", "plate", "tip_rack", "plates", From b5cd181ac1c0b2704fa9931c3da1a4611260edb0 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:59:45 +0800 Subject: [PATCH 07/12] add isFlapY --- unilabos/resources/resource_tracker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/unilabos/resources/resource_tracker.py b/unilabos/resources/resource_tracker.py index 8ade8e1f..2d429a2e 100644 --- a/unilabos/resources/resource_tracker.py +++ b/unilabos/resources/resource_tracker.py @@ -81,6 +81,7 @@ class ResourceDictPoseExtraObjectType(BaseModel): class ResourceDictPoseExtraObject(BaseModel): z_index: Optional[int] = Field(alias="zIndex", default=None) + is_flap_y: Optional[bool] = Field(alias="isFlapY", default=None) class ResourceDictPositionType(TypedDict): From e0fae94c1003e91fa6af8ce828208517af3d67c9 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:06:41 +0800 Subject: [PATCH 08/12] change pose extra to any --- unilabos/resources/resource_tracker.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/unilabos/resources/resource_tracker.py b/unilabos/resources/resource_tracker.py index 2d429a2e..bb5614a3 100644 --- a/unilabos/resources/resource_tracker.py +++ b/unilabos/resources/resource_tracker.py @@ -75,15 +75,6 @@ class ResourceDictPositionObject(BaseModel): z: float = Field(description="Z coordinate", default=0.0) -class ResourceDictPoseExtraObjectType(BaseModel): - z_index: int - - -class ResourceDictPoseExtraObject(BaseModel): - z_index: Optional[int] = Field(alias="zIndex", default=None) - is_flap_y: Optional[bool] = Field(alias="isFlapY", default=None) - - class ResourceDictPositionType(TypedDict): size: ResourceDictPositionSizeType scale: ResourceDictPositionScaleType @@ -110,7 +101,7 @@ class ResourceDictPosition(BaseModel): cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"] = Field( description="Cross section type", default="rectangle" ) - extra: Optional[ResourceDictPoseExtraObject] = Field(description="Extra data", default=None) + extra: Optional[Dict[str, Any]] = Field(description="Extra data", default=None) class ResourceDictType(TypedDict): From b993c1f590280522d8181fef937542058799c629 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:18:09 +0800 Subject: [PATCH 09/12] add gzip --- unilabos/resources/resource_tracker.py | 29 +++++++++++++++++++------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/unilabos/resources/resource_tracker.py b/unilabos/resources/resource_tracker.py index bb5614a3..b34d10cc 100644 --- a/unilabos/resources/resource_tracker.py +++ b/unilabos/resources/resource_tracker.py @@ -840,14 +840,27 @@ class ResourceTreeSet(object): f"从远端同步了 {added_count} 个物料子树" ) else: - # 情况2: 二级是物料(不是 device) - if remote_child_name not in local_children_map: - # 引入整个子树 - remote_child.res_content.parent = local_device.res_content - local_device.children.append(remote_child) - logger.info(f"Device '{remote_root_id}': 从远端同步物料子树 '{remote_child_name}'") - else: - logger.info(f"物料 '{remote_root_id}/{remote_child_name}' 已存在,跳过") + # 二级物料已存在,比较三级子节点是否缺失 + local_material = local_children_map[remote_child_name] + local_material_children_map = {child.res_content.name: child for child in + local_material.children} + added_count = 0 + for remote_sub in remote_child.children: + remote_sub_name = remote_sub.res_content.name + if remote_sub_name not in local_material_children_map: + remote_sub.res_content.parent = local_material.res_content + local_material.children.append(remote_sub) + added_count += 1 + else: + logger.info( + f"物料 '{remote_root_id}/{remote_child_name}/{remote_sub_name}' " + f"已存在,跳过" + ) + if added_count > 0: + logger.info( + f"物料 '{remote_root_id}/{remote_child_name}': " + f"从远端同步了 {added_count} 个子物料" + ) else: # 情况1: 一级节点是物料(不是 device) # 检查是否已存在 From cadbe87e3f0b2ddb1d2f83a59c2cc858abdc87ad Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:18:19 +0800 Subject: [PATCH 10/12] add gzip --- unilabos/app/web/client.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/unilabos/app/web/client.py b/unilabos/app/web/client.py index b43b0f44..75b9e343 100644 --- a/unilabos/app/web/client.py +++ b/unilabos/app/web/client.py @@ -3,7 +3,7 @@ HTTP客户端模块 提供与远程服务器通信的客户端功能,只有host需要用 """ - +import gzip import json import os from typing import List, Dict, Any, Optional @@ -290,10 +290,17 @@ class HTTPClient: Returns: Response: API响应对象 """ + compressed_body = gzip.compress( + json.dumps(registry_data, ensure_ascii=False, default=str).encode("utf-8") + ) response = requests.post( f"{self.remote_addr}/lab/resource", - json=registry_data, - headers={"Authorization": f"Lab {self.auth}"}, + data=compressed_body, + headers={ + "Authorization": f"Lab {self.auth}", + "Content-Type": "application/json", + "Content-Encoding": "gzip", + }, timeout=30, ) if response.status_code not in [200, 201]: From 975a56415a0f090b22ca7bd1759bb5ea71510e03 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Wed, 4 Mar 2026 19:18:19 +0800 Subject: [PATCH 11/12] import gzip --- unilabos/app/web/client.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/unilabos/app/web/client.py b/unilabos/app/web/client.py index b43b0f44..75b9e343 100644 --- a/unilabos/app/web/client.py +++ b/unilabos/app/web/client.py @@ -3,7 +3,7 @@ HTTP客户端模块 提供与远程服务器通信的客户端功能,只有host需要用 """ - +import gzip import json import os from typing import List, Dict, Any, Optional @@ -290,10 +290,17 @@ class HTTPClient: Returns: Response: API响应对象 """ + compressed_body = gzip.compress( + json.dumps(registry_data, ensure_ascii=False, default=str).encode("utf-8") + ) response = requests.post( f"{self.remote_addr}/lab/resource", - json=registry_data, - headers={"Authorization": f"Lab {self.auth}"}, + data=compressed_body, + headers={ + "Authorization": f"Lab {self.auth}", + "Content-Type": "application/json", + "Content-Encoding": "gzip", + }, timeout=30, ) if response.status_code not in [200, 201]: From f2c0bec02c0e9bdabe83e3653719c52ff954a3c3 Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Sat, 7 Mar 2026 04:40:34 +0800 Subject: [PATCH 12/12] add websocket connection timeout and improve reconnection logic add open_timeout parameter to websocket connection add TimeoutError and InvalidStatus exception handling implement exponential backoff for reconnection attempts simplify reconnection logic flow --- unilabos/app/ws_client.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/unilabos/app/ws_client.py b/unilabos/app/ws_client.py index f2235e94..35b4766a 100644 --- a/unilabos/app/ws_client.py +++ b/unilabos/app/ws_client.py @@ -466,6 +466,7 @@ class MessageProcessor: async with websockets.connect( self.websocket_url, ssl=ssl_context, + open_timeout=20, ping_interval=WSConfig.ping_interval, ping_timeout=10, additional_headers={ @@ -497,6 +498,18 @@ class MessageProcessor: except websockets.exceptions.ConnectionClosed: logger.warning("[MessageProcessor] Connection closed") self.connected = False + except TimeoutError: + logger.warning( + f"[MessageProcessor] Connection timeout (attempt {self.reconnect_count + 1}), " + f"server may be temporarily unavailable" + ) + self.connected = False + except websockets.exceptions.InvalidStatus as e: + logger.warning( + f"[MessageProcessor] Server returned unexpected HTTP status {e.response.status_code}, " + f"WebSocket endpoint may not be ready yet" + ) + self.connected = False except Exception as e: logger.error(f"[MessageProcessor] Connection error: {str(e)}") logger.error(traceback.format_exc()) @@ -505,18 +518,19 @@ class MessageProcessor: self.websocket = None # 重连逻辑 - if self.is_running and self.reconnect_count < WSConfig.max_reconnect_attempts: + if not self.is_running: + break + if self.reconnect_count < WSConfig.max_reconnect_attempts: self.reconnect_count += 1 + backoff = min(WSConfig.reconnect_interval * (2 ** (self.reconnect_count - 1)), 60) logger.info( - f"[MessageProcessor] Reconnecting in {WSConfig.reconnect_interval}s " + f"[MessageProcessor] Reconnecting in {backoff}s " f"(attempt {self.reconnect_count}/{WSConfig.max_reconnect_attempts})" ) - await asyncio.sleep(WSConfig.reconnect_interval) - elif self.reconnect_count >= WSConfig.max_reconnect_attempts: + await asyncio.sleep(backoff) + else: logger.error("[MessageProcessor] Max reconnection attempts reached") break - else: - self.reconnect_count -= 1 async def _message_handler(self): """处理接收到的消息"""