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 +