Merge pull request #250 from ALITTLELZ/adaptors

Add PRCXI functional modules and fix Deck layout
This commit is contained in:
q434343
2026-03-26 12:27:06 +08:00
committed by GitHub
7 changed files with 632 additions and 242 deletions

View File

@@ -45,6 +45,7 @@ from pylabrobot.resources import (
Trash,
PlateAdapter,
TubeRack,
create_homogeneous_resources,
)
from unilabos.devices.liquid_handling.liquid_handler_abstract import (
@@ -55,6 +56,7 @@ from unilabos.devices.liquid_handling.liquid_handler_abstract import (
TransferLiquidReturn,
)
from unilabos.registry.placeholder_type import ResourceSlot
from unilabos.resources.itemized_carrier import ItemizedCarrier
from unilabos.resources.resource_tracker import ResourceTreeSet
from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode
@@ -91,15 +93,15 @@ 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"]
_DEFAULT_CONTENT_TYPE = ["plate", "tip_rack", "plates", "tip_racks", "tube_rack", "adaptor", "plateadapter", "module"]
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
sites: Optional[List[Dict[str, Any]]] = None, **kwargs):
@@ -544,6 +546,108 @@ class PRCXI9300TubeRack(TubeRack):
return data
class PRCXI9300ModuleSite(ItemizedCarrier):
"""
PRCXI 功能模块的基础站点类(加热/冷却/震荡/磁吸等)。
- 继承 ItemizedCarrier可被拖放到 Deck 槽位上
- 顶面有一个 ResourceHolder 站点,可吸附板类资源(叠放)
- content_type 包含 "plateadapter" 以支持适配器叠放
- 支持 material_info 注入
"""
def __init__(self, name: str, size_x: float, size_y: float, size_z: float,
material_info: Optional[Dict[str, Any]] = None, **kwargs):
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_dict = {name: sites}
content_type = [
"plate",
"tip_rack",
"plates",
"tip_racks",
"tube_rack",
"plateadapter",
]
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):
from pylabrobot.resources.resource import Resource
Resource.assign_child_resource(self, resource, location=location, reassign=reassign)
def unassign_child_resource(self, resource):
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 = {}
if hasattr(self, 'sites') and self.sites:
sites_info = []
for site in self.sites:
if hasattr(site, '__class__') and 'pylabrobot' in str(site.__class__.__module__):
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
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():
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
def load_state(self, state: Dict[str, Any]) -> None:
super().load_state(state)
if 'sites' in state:
self.sites = [state['sites']]
class PRCXI9300PlateAdapter(PlateAdapter):
"""
专用板式适配器类:用于承载 Plate 的底座(如 PCR 适配器、磁吸架等)。

View File

@@ -0,0 +1,150 @@
from typing import Any, Dict, Optional
from .prcxi import PRCXI9300ModuleSite
class PRCXI9300FunctionalModule(PRCXI9300ModuleSite):
"""
PRCXI 9300 功能模块基类(加热/冷却/震荡/加热震荡/磁吸等)。
设计目标:
- 作为一个可以在工作台上拖拽摆放的实体资源(继承自 PRCXI9300ModuleSite -> ItemizedCarrier
- 顶面存在一个站点site可吸附标准板类资源plate / tip_rack / tube_rack 等)。
- 支持注入 `material_info` (UUID 等),并且在 serialize_state 时做安全过滤。
"""
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,
category=category,
**kwargs,
)
# 记录模块类型(加热 / 冷却 / 震荡 / 加热震荡 / 磁吸)
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
# ============================================================================
# 具体功能模块定义
# 这里的尺寸和 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,
},
)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"
]
}
]

View File

@@ -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"
]
}
]