From ca985f92abfdbb45d82fdc00b2db4b59734f18d7 Mon Sep 17 00:00:00 2001 From: ALITTLELZ Date: Mon, 2 Mar 2026 14:35:12 +0800 Subject: [PATCH 1/5] Add 'plateadapter' to device and test configurations --- .../devices/liquid_handling/prcxi/prcxi.py | 3 +- .../experiments/prcxi_9320_with_res_test.json | 48 ++++++++++++------- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/unilabos/devices/liquid_handling/prcxi/prcxi.py b/unilabos/devices/liquid_handling/prcxi/prcxi.py index 0ff0dc4c..28c4063a 100644 --- a/unilabos/devices/liquid_handling/prcxi/prcxi.py +++ b/unilabos/devices/liquid_handling/prcxi/prcxi.py @@ -540,7 +540,8 @@ class PRCXI9300PlateAdapterSite(ItemizedCarrier): "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "plateadapter" ] # 如果提供了sites参数,则用sites_in中的值替换sites_dict中对应的元素 if sites_in is not None and isinstance(sites_in, dict): diff --git a/unilabos/test/experiments/prcxi_9320_with_res_test.json b/unilabos/test/experiments/prcxi_9320_with_res_test.json index 831cecce..2cf9ffb6 100644 --- a/unilabos/test/experiments/prcxi_9320_with_res_test.json +++ b/unilabos/test/experiments/prcxi_9320_with_res_test.json @@ -108,7 +108,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "plateadapter" ] } ] @@ -153,7 +154,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "plateadapter" ] } ] @@ -198,7 +200,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "plateadapter" ] } ] @@ -243,7 +246,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "plateadapter" ] } ] @@ -288,7 +292,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "plateadapter" ] } ] @@ -333,7 +338,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "plateadapter" ] } ] @@ -378,7 +384,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "plateadapter" ] } ] @@ -423,7 +430,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "plateadapter" ] } ] @@ -468,7 +476,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "plateadapter" ] } ] @@ -513,7 +522,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "plateadapter" ] } ] @@ -558,7 +568,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "plateadapter" ] } ] @@ -603,7 +614,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "plateadapter" ] } ] @@ -648,7 +660,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "plateadapter" ] } ] @@ -693,7 +706,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "plateadapter" ] } ] @@ -738,7 +752,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "plateadapter" ] } ] @@ -783,7 +798,8 @@ "tip_rack", "plates", "tip_racks", - "tube_rack" + "tube_rack", + "plateadapter" ] } ] From a187a57430e74f6ad784ed8c0a956ed4619c5073 Mon Sep 17 00:00:00 2001 From: ALITTLELZ Date: Wed, 25 Mar 2026 15:19:48 +0800 Subject: [PATCH 2/5] Add PRCXI functional modules (heating/cooling/shaking/magnetic) and registry config Co-Authored-By: Claude Opus 4.6 --- .../liquid_handling/prcxi/prcxi_modules.py | 189 ++++++++++++++++++ .../registry/resources/prcxi/modules.yaml | 70 +++++++ 2 files changed, 259 insertions(+) create mode 100644 unilabos/devices/liquid_handling/prcxi/prcxi_modules.py create mode 100644 unilabos/registry/resources/prcxi/modules.yaml diff --git a/unilabos/devices/liquid_handling/prcxi/prcxi_modules.py b/unilabos/devices/liquid_handling/prcxi/prcxi_modules.py new file mode 100644 index 00000000..35935024 --- /dev/null +++ b/unilabos/devices/liquid_handling/prcxi/prcxi_modules.py @@ -0,0 +1,189 @@ +from typing import Any, Dict, Optional + +from .prcxi import PRCXI9300PlateAdapterSite + + +class PRCXI9300FunctionalModule(PRCXI9300PlateAdapterSite): + """ + PRCXI 9300 功能模块基类(加热/冷却/震荡/加热震荡/磁吸等)。 + + 设计目标: + - 作为一个可以在工作台上拖拽摆放的实体资源(继承自 ItemizedCarrier)。 + - 顶面存在一个站点(site),可吸附标准板类资源(plate / tip_rack / tube_rack 等), + 站点的行为参考 `PRCXI9300PlateAdapterSite` 的实现。 + - 支持注入 `material_info` (UUID 等),并且在 serialize_state 时做安全过滤, + 行为与 `PRCXI9300PlateAdapter` 保持一致。 + """ + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + module_type: Optional[str] = None, + category: str = "module", + model: Optional[str] = None, + material_info: Optional[Dict[str, Any]] = None, + **kwargs: Any, + ): + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + material_info=material_info, + model=model, + **kwargs, + ) + + # 作为一个“可被槽位吸附”的实体,在 PLR 层面上将其视为 plate_adapter, + # 这样与已有的槽位/适配器逻辑兼容;模块语义通过 _unilabos_state 中的 + # category/module_type 表达。 + try: + self.category = "plate_adapter" + except Exception: + # category 不是硬性要求属性,失败时静默忽略 + pass + + # 记录模块类型(加热 / 冷却 / 震荡 / 加热震荡 / 磁吸) + # - 通过工厂函数创建时,会显式传入 module_type + # - 通过反序列化(deserialize)创建时,可能没有该字段,此时允许为 None + self.module_type = module_type or "generic" + + # 与 PRCXI9300PlateAdapter 一致,使用 _unilabos_state 保存扩展信息 + if not hasattr(self, "_unilabos_state") or self._unilabos_state is None: + self._unilabos_state = {} + + # super().__init__ 已经在有 material_info 时写入 "Material",这里仅确保存在 + if material_info is not None and "Material" not in self._unilabos_state: + self._unilabos_state["Material"] = material_info + + # 额外标记 category 和模块类型,便于前端或上层逻辑区分 + self._unilabos_state.setdefault("category", category) + self._unilabos_state["module_type"] = module_type + + def serialize_state(self) -> Dict[str, Dict[str, Any]]: + """ + 在父类基础上,增加对 _unilabos_state 的安全序列化, + 行为与 `PRCXI9300PlateAdapter.serialize_state` 保持一致。 + """ + try: + data = super().serialize_state() + except AttributeError: + data = {} + + if hasattr(self, "_unilabos_state") and self._unilabos_state: + safe_state: Dict[str, Any] = {} + for k, v in self._unilabos_state.items(): + if k == "Material" and isinstance(v, dict): + safe_material: Dict[str, Any] = {} + for mk, mv in v.items(): + # 只保留基本数据类型 (字符串, 数字, 布尔值, 列表, 字典, None) + if isinstance(mv, (str, int, float, bool, list, dict, type(None))): + safe_material[mk] = mv + safe_state[k] = safe_material + elif isinstance(v, (str, int, float, bool, list, dict, type(None))): + safe_state[k] = v + + data.update(safe_state) + + return data + + +# ============================================================================ +# 具体功能模块定义 +# 这里的尺寸和 material_info 目前为占位参数,后续可根据实际测量/JSON 配置进行更新。 +# 顶面站点尺寸与模块外形一致,保证可以吸附标准 96 板/储液槽等。 +# ============================================================================ + + +def PRCXI_Heating_Module(name: str) -> PRCXI9300FunctionalModule: + """加热模块(顶面可吸附标准板)。""" + return PRCXI9300FunctionalModule( + name=name, + size_x=127.76, + size_y=85.48, + size_z=40.0, + module_type="heating", + model="PRCXI_Heating_Module", + material_info={ + "uuid": "TODO-HEATING-MODULE-UUID", + "Code": "HEAT-MOD", + "Name": "PRCXI 加热模块", + "SupplyType": 3, + }, + ) + + +def PRCXI_MetalCooling_Module(name: str) -> PRCXI9300FunctionalModule: + """金属冷却模块(顶面可吸附标准板)。""" + return PRCXI9300FunctionalModule( + name=name, + size_x=127.76, + size_y=85.48, + size_z=40.0, + module_type="metal_cooling", + model="PRCXI_MetalCooling_Module", + material_info={ + "uuid": "TODO-METAL-COOLING-MODULE-UUID", + "Code": "METAL-COOL-MOD", + "Name": "PRCXI 金属冷却模块", + "SupplyType": 3, + }, + ) + + +def PRCXI_Shaking_Module(name: str) -> PRCXI9300FunctionalModule: + """震荡模块(顶面可吸附标准板)。""" + return PRCXI9300FunctionalModule( + name=name, + size_x=127.76, + size_y=85.48, + size_z=50.0, + module_type="shaking", + model="PRCXI_Shaking_Module", + material_info={ + "uuid": "TODO-SHAKING-MODULE-UUID", + "Code": "SHAKE-MOD", + "Name": "PRCXI 震荡模块", + "SupplyType": 3, + }, + ) + + +def PRCXI_Heating_Shaking_Module(name: str) -> PRCXI9300FunctionalModule: + """加热震荡模块(顶面可吸附标准板)。""" + return PRCXI9300FunctionalModule( + name=name, + size_x=127.76, + size_y=85.48, + size_z=55.0, + module_type="heating_shaking", + model="PRCXI_Heating_Shaking_Module", + material_info={ + "uuid": "TODO-HEATING-SHAKING-MODULE-UUID", + "Code": "HEAT-SHAKE-MOD", + "Name": "PRCXI 加热震荡模块", + "SupplyType": 3, + }, + ) + + +def PRCXI_Magnetic_Module(name: str) -> PRCXI9300FunctionalModule: + """磁吸模块(顶面可吸附标准板)。""" + return PRCXI9300FunctionalModule( + name=name, + size_x=127.76, + size_y=85.48, + size_z=30.0, + module_type="magnetic", + model="PRCXI_Magnetic_Module", + material_info={ + "uuid": "TODO-MAGNETIC-MODULE-UUID", + "Code": "MAG-MOD", + "Name": "PRCXI 磁吸模块", + "SupplyType": 3, + }, + ) + diff --git a/unilabos/registry/resources/prcxi/modules.yaml b/unilabos/registry/resources/prcxi/modules.yaml new file mode 100644 index 00000000..b35a6ece --- /dev/null +++ b/unilabos/registry/resources/prcxi/modules.yaml @@ -0,0 +1,70 @@ +PRCXI_Heating_Module: + category: + - prcxi + - modules + class: + module: unilabos.devices.liquid_handling.prcxi.prcxi_modules:PRCXI_Heating_Module + type: pylabrobot + description: '加热模块 (Code: HEAT-MOD)' + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 + +PRCXI_MetalCooling_Module: + category: + - prcxi + - modules + class: + module: unilabos.devices.liquid_handling.prcxi.prcxi_modules:PRCXI_MetalCooling_Module + type: pylabrobot + description: '金属冷却模块 (Code: METAL-COOL-MOD)' + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 + +PRCXI_Shaking_Module: + category: + - prcxi + - modules + class: + module: unilabos.devices.liquid_handling.prcxi.prcxi_modules:PRCXI_Shaking_Module + type: pylabrobot + description: '震荡模块 (Code: SHAKE-MOD)' + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 + +PRCXI_Heating_Shaking_Module: + category: + - prcxi + - modules + class: + module: unilabos.devices.liquid_handling.prcxi.prcxi_modules:PRCXI_Heating_Shaking_Module + type: pylabrobot + description: '加热震荡模块 (Code: HEAT-SHAKE-MOD)' + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 + +PRCXI_Magnetic_Module: + category: + - prcxi + - modules + class: + module: unilabos.devices.liquid_handling.prcxi.prcxi_modules:PRCXI_Magnetic_Module + type: pylabrobot + description: '磁吸模块 (Code: MAG-MOD)' + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 + From 0c667e68e6acbd215753917cce1621aebc684de0 Mon Sep 17 00:00:00 2001 From: ALITTLELZ Date: Wed, 25 Mar 2026 16:22:39 +0800 Subject: [PATCH 3/5] Remove deprecated PRCXI9300PlateAdapterSite, replaced by PRCXI9300ModuleSite PRCXI9300PlateAdapterSite was already removed by upstream/prcix9320. Its functionality is now provided by PRCXI9300ModuleSite which serves as the base class for functional modules (heating/cooling/shaking/magnetic). Co-Authored-By: Claude Opus 4.6 --- .../devices/liquid_handling/prcxi/prcxi.py | 94 ------------------- 1 file changed, 94 deletions(-) diff --git a/unilabos/devices/liquid_handling/prcxi/prcxi.py b/unilabos/devices/liquid_handling/prcxi/prcxi.py index ffe898b2..6d4922ee 100644 --- a/unilabos/devices/liquid_handling/prcxi/prcxi.py +++ b/unilabos/devices/liquid_handling/prcxi/prcxi.py @@ -546,100 +546,6 @@ class PRCXI9300TubeRack(TubeRack): return data -class PRCXI9300PlateAdapterSite(ItemizedCarrier): - def __init__(self, name: str, size_x: float, size_y: float, size_z: float, - material_info: Optional[Dict[str, Any]] = None, **kwargs): - # 处理 sites 参数的不同格式 - - sites = create_homogeneous_resources( - klass=ResourceHolder, - locations=[Coordinate(0, 0, 0)], - resource_size_x=size_x, - resource_size_y=size_y, - resource_size_z=size_z, - name_prefix=name, - )[0] - - # 确保不传递重复的参数 - kwargs.pop('layout', None) - sites_in = kwargs.pop('sites', None) - - # 创建默认的sites字典 - sites_dict = {name: sites} - # 优先从 sites_in 读取 'content_type',否则使用默认值 - - content_type = [ - "plate", - "tip_rack", - "plates", - "tip_racks", - "tube_rack", - "plateadapter" - ] - # 如果提供了sites参数,则用sites_in中的值替换sites_dict中对应的元素 - if sites_in is not None and isinstance(sites_in, dict): - for site_key, site_value in sites_in.items(): - if site_key in sites_dict: - sites_dict[site_key] = site_value - - super().__init__(name, size_x, size_y, size_z, - sites=sites_dict, - num_items_x=kwargs.pop('num_items_x', 1), - num_items_y=kwargs.pop('num_items_y', 1), - num_items_z=kwargs.pop('num_items_z', 1), - content_type=content_type, - **kwargs) - self._unilabos_state = {} - if material_info: - self._unilabos_state["Material"] = material_info - - - def assign_child_resource(self, resource, location=Coordinate(0, 0, 0), reassign=True, spot=None): - """重写 assign_child_resource 方法,对于适配器位置,不使用索引分配""" - # 直接调用 Resource 的 assign_child_resource,避免 ItemizedCarrier 的索引逻辑 - from pylabrobot.resources.resource import Resource - Resource.assign_child_resource(self, resource, location=location, reassign=reassign) - - def unassign_child_resource(self, resource): - """重写 unassign_child_resource 方法,对于适配器位置,不使用 sites 列表""" - # 直接调用 Resource 的 unassign_child_resource,避免 ItemizedCarrier 的 sites 逻辑 - from pylabrobot.resources.resource import Resource - Resource.unassign_child_resource(self, resource) - - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - try: - data = super().serialize_state() - except AttributeError: - data = {} - - # 包含 sites 配置信息,但避免序列化 ResourceHolder 对象 - if hasattr(self, 'sites') and self.sites: - # 只保存 sites 的基本信息,不保存 ResourceHolder 对象本身 - sites_info = [] - for site in self.sites: - if hasattr(site, '__class__') and 'pylabrobot' in str(site.__class__.__module__): - # 对于 pylabrobot 对象,只保存基本信息 - sites_info.append({ - "__pylabrobot_object__": True, - "class": site.__class__.__name__, - "module": site.__class__.__module__, - "name": getattr(site, 'name', str(site)) - }) - else: - sites_info.append(site) - data['sites'] = sites_info - - return data - - def load_state(self, state: Dict[str, Any]) -> None: - """加载状态,包括 sites 配置信息""" - super().load_state(state) - - # 从状态中恢复 sites 配置信息 - if 'sites' in state: - self.sites = [state['sites']] - - class PRCXI9300ModuleSite(ItemizedCarrier): """ PRCXI 功能模块的基础站点类(加热/冷却/震荡/磁吸等)。 From 7f4b57f589d5d8aa6cd958e7f7b5417558bdc938 Mon Sep 17 00:00:00 2001 From: ALITTLELZ Date: Wed, 25 Mar 2026 17:16:04 +0800 Subject: [PATCH 4/5] Fix Deck slot Y-axis inversion: T1 should be top-left, not bottom-left Upstream rewrite of PRCXI9300Deck lost the Y-axis flip logic from the original `(3-row)*96+13` formula. T1-T4 were rendered at the bottom instead of the top. Reversed _DEFAULT_SITE_POSITIONS Y coordinates and updated prcxi_9320_slim.json accordingly. Also added "plateadapter" and "module" to slim JSON content_type entries. Co-Authored-By: Claude Opus 4.6 --- .../devices/liquid_handling/prcxi/prcxi.py | 10 +- .../test/experiments/prcxi_9320_slim.json | 472 ++++++++++-------- 2 files changed, 257 insertions(+), 225 deletions(-) diff --git a/unilabos/devices/liquid_handling/prcxi/prcxi.py b/unilabos/devices/liquid_handling/prcxi/prcxi.py index 6d4922ee..3b7c7b13 100644 --- a/unilabos/devices/liquid_handling/prcxi/prcxi.py +++ b/unilabos/devices/liquid_handling/prcxi/prcxi.py @@ -93,12 +93,12 @@ class PRCXI9300Deck(Deck): 该类定义了 PRCXI 9300 的工作台布局和槽位信息。 """ - # T1-T16 默认位置 (4列×4行) + # T1-T16 默认位置 (4列×4行, Y轴从上往下递减, T1在左上角) _DEFAULT_SITE_POSITIONS = [ - (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 + (0, 288, 0), (138, 288, 0), (276, 288, 0), (414, 288, 0), # T1-T4 (第1行, 最上) + (0, 192, 0), (138, 192, 0), (276, 192, 0), (414, 192, 0), # T5-T8 (第2行) + (0, 96, 0), (138, 96, 0), (276, 96, 0), (414, 96, 0), # T9-T12 (第3行) + (0, 0, 0), (138, 0, 0), (276, 0, 0), (414, 0, 0), # T13-T16 (第4行, 最下) ] _DEFAULT_SITE_SIZE = {"width": 128.0, "height": 86, "depth": 0} _DEFAULT_CONTENT_TYPE = ["plate", "tip_rack", "plates", "tip_racks", "tube_rack", "adaptor", "plateadapter", "module"] diff --git a/unilabos/test/experiments/prcxi_9320_slim.json b/unilabos/test/experiments/prcxi_9320_slim.json index 2aaee6a7..1ad470d1 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": 0, + "y": 288, "z": 0 }, "size": { @@ -89,7 +89,9 @@ "plates", "tip_racks", "tube_rack", - "adaptor" + "adaptor", + "plateadapter", + "module" ] }, { @@ -98,7 +100,7 @@ "occupied_by": null, "position": { "x": 138, - "y": 0, + "y": 288, "z": 0 }, "size": { @@ -112,7 +114,9 @@ "plates", "tip_racks", "tube_rack", - "adaptor" + "adaptor", + "plateadapter", + "module" ] }, { @@ -121,7 +125,7 @@ "occupied_by": null, "position": { "x": 276, - "y": 0, + "y": 288, "z": 0 }, "size": { @@ -135,7 +139,9 @@ "plates", "tip_racks", "tube_rack", - "adaptor" + "adaptor", + "plateadapter", + "module" ] }, { @@ -144,6 +150,231 @@ "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "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" + ] + }, + { + "label": "T13", + "visible": true, + "occupied_by": null, + "position": { + "x": 0, "y": 0, "z": 0 }, @@ -158,214 +389,9 @@ "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 - }, - "size": { - "width": 128.0, - "height": 86, - "depth": 0 - }, - "content_type": [ - "plate", - "tip_rack", - "plates", - "tip_racks", - "tube_rack", - "adaptor" + "adaptor", + "plateadapter", + "module" ] }, { @@ -374,7 +400,7 @@ "occupied_by": null, "position": { "x": 138, - "y": 288, + "y": 0, "z": 0 }, "size": { @@ -388,7 +414,9 @@ "plates", "tip_racks", "tube_rack", - "adaptor" + "adaptor", + "plateadapter", + "module" ] }, { @@ -397,7 +425,7 @@ "occupied_by": null, "position": { "x": 276, - "y": 288, + "y": 0, "z": 0 }, "size": { @@ -411,7 +439,9 @@ "plates", "tip_racks", "tube_rack", - "adaptor" + "adaptor", + "plateadapter", + "module" ] }, { @@ -420,7 +450,7 @@ "occupied_by": null, "position": { "x": 414, - "y": 288, + "y": 0, "z": 0 }, "size": { @@ -434,7 +464,9 @@ "plates", "tip_racks", "tube_rack", - "adaptor" + "adaptor", + "plateadapter", + "module" ] } ] From 71d35d31afbf08b993dd7f539c7bc31dbd1f02f0 Mon Sep 17 00:00:00 2001 From: ALITTLELZ Date: Wed, 25 Mar 2026 18:32:01 +0800 Subject: [PATCH 5/5] Register PRCXI9300ModuleSite/FunctionalModule for PLR deserialization Added PRCXI9300ModuleSite and PRCXI9300FunctionalModule to the PLR class registration in plr_additional_res_reg.py so find_subclass can locate them during deserialization of cached cloud data. Also added "module" and "carrier" to replace_plr_type and TYPE_MAP in resource_tracker.py to suppress unknown type warnings. Co-Authored-By: Claude Opus 4.6 --- unilabos/resources/plr_additional_res_reg.py | 3 +++ unilabos/resources/resource_tracker.py | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/unilabos/resources/plr_additional_res_reg.py b/unilabos/resources/plr_additional_res_reg.py index 1c019ded..a4be086e 100644 --- a/unilabos/resources/plr_additional_res_reg.py +++ b/unilabos/resources/plr_additional_res_reg.py @@ -9,6 +9,9 @@ def register(): from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300TipRack from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300Trash from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300TubeRack + from unilabos.devices.liquid_handling.prcxi.prcxi import PRCXI9300ModuleSite + # noinspection PyUnresolvedReferences + from unilabos.devices.liquid_handling.prcxi.prcxi_modules import PRCXI9300FunctionalModule # noinspection PyUnresolvedReferences from unilabos.devices.workstation.workstation_base import WorkStationContainer diff --git a/unilabos/resources/resource_tracker.py b/unilabos/resources/resource_tracker.py index 288ddc12..6a0755b5 100644 --- a/unilabos/resources/resource_tracker.py +++ b/unilabos/resources/resource_tracker.py @@ -459,6 +459,8 @@ class ResourceTreeSet(object): "reagent_bottle": "reagent_bottle", "flask": "flask", "beaker": "beaker", + "module": "module", + "carrier": "carrier", } if source in replace_info: return replace_info[source] @@ -596,6 +598,8 @@ class ResourceTreeSet(object): "deck": "Deck", "container": "RegularContainer", "tip_spot": "TipSpot", + "module": "PRCXI9300ModuleSite", + "carrier": "ItemizedCarrier", } def collect_node_data(node: ResourceDictInstance, name_to_uuid: dict, all_states: dict, name_to_extra: dict): @@ -958,6 +962,17 @@ class ResourceTreeSet(object): f"从远端同步了 {added_count} 个物料子树" ) else: + # 二级是物料 + if remote_child_name not in local_children_map: + # 本地不存在该物料,直接引入 + remote_child.res_content.parent = local_device.res_content + local_device.children.append(remote_child) + local_children_map[remote_child_name] = remote_child + logger.info( + f"物料 '{remote_root_id}/{remote_child_name}': " + f"从远端同步了整个子树" + ) + continue # 二级物料已存在,比较三级子节点是否缺失 local_material = local_children_map[remote_child_name] local_material_children_map = {child.res_content.name: child for child in