From 0dc2488f020d90e68c35a4cc4b176a6237e7313f Mon Sep 17 00:00:00 2001 From: h840473807 <47357934+h840473807@users.noreply.github.com> Date: Tue, 23 Sep 2025 01:18:04 +0800 Subject: [PATCH 01/17] coin_cell_station draft --- .../button_battery_station.py | 1292 ++ .../coin_cell_assembly/coin_cell_assembly.py | 1147 +- .../coin_cell_assembly/new_cellconfig4.json | 14472 ++++++++++++++++ 3 files changed, 16889 insertions(+), 22 deletions(-) create mode 100644 unilabos/devices/workstation/coin_cell_assembly/button_battery_station.py create mode 100644 unilabos/devices/workstation/coin_cell_assembly/new_cellconfig4.json diff --git a/unilabos/devices/workstation/coin_cell_assembly/button_battery_station.py b/unilabos/devices/workstation/coin_cell_assembly/button_battery_station.py new file mode 100644 index 00000000..676141aa --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/button_battery_station.py @@ -0,0 +1,1292 @@ +""" +纽扣电池组装工作站物料类定义 +Button Battery Assembly Station Resource Classes +""" + +from __future__ import annotations + +from collections import OrderedDict +from typing import Any, Dict, List, Optional, TypedDict, Union, cast + +from pylabrobot.resources.coordinate import Coordinate +from pylabrobot.resources.container import Container +from pylabrobot.resources.deck import Deck +from pylabrobot.resources.itemized_resource import ItemizedResource +from pylabrobot.resources.resource import Resource +from pylabrobot.resources.resource_stack import ResourceStack +from pylabrobot.resources.tip_rack import TipRack, TipSpot +from pylabrobot.resources.trash import Trash +from pylabrobot.resources.utils import create_ordered_items_2d + + +class ElectrodeSheetState(TypedDict): + diameter: float # 直径 (mm) + thickness: float # 厚度 (mm) + mass: float # 质量 (g) + material_type: str # 材料类型(正极、负极、隔膜、弹片、垫片、铝箔等) + info: Optional[str] # 附加信息 + +class ElectrodeSheet(Resource): + """极片类 - 包含正负极片、隔膜、弹片、垫片、铝箔等所有片状材料""" + + def __init__( + self, + name: str = "极片", + size_x=10, + size_y=10, + size_z=10, + category: str = "electrode_sheet", + model: Optional[str] = None, + ): + """初始化极片 + + Args: + name: 极片名称 + category: 类别 + model: 型号 + """ + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + category=category, + model=model, + ) + self._unilabos_state: ElectrodeSheetState = ElectrodeSheetState( + diameter=14, + thickness=0.1, + mass=0.5, + material_type="copper", + info=None + ) + + # TODO: 这个还要不要?给self._unilabos_state赋值的? + def load_state(self, state: Dict[str, Any]) -> None: + """格式不变""" + super().load_state(state) + self._unilabos_state = state + #序列化 + def serialize_state(self) -> Dict[str, Dict[str, Any]]: + """格式不变""" + data = super().serialize_state() + data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) + return data + +# TODO: 这个应该只能放一个极片 +class MaterialHoleState(TypedDict): + diameter: int + depth: int + max_sheets: int + info: Optional[str] # 附加信息 + +class MaterialHole(Resource): + """料板洞位类""" + children: List[ElectrodeSheet] = [] + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + category: str = "material_hole", + **kwargs + ): + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + category=category, + ) + self._unilabos_state: MaterialHoleState = MaterialHoleState( + diameter=20, + depth=10, + max_sheets=1, + info=None + ) + + def get_all_sheet_info(self): + info_list = [] + for sheet in self.children: + info_list.append(sheet._unilabos_state["info"]) + return info_list + + #这个函数函数好像没用,一般不会集中赋值质量 + def set_all_sheet_mass(self): + for sheet in self.children: + sheet._unilabos_state["mass"] = 0.5 # 示例:设置质量为0.5g + + def load_state(self, state: Dict[str, Any]) -> None: + """格式不变""" + super().load_state(state) + self._unilabos_state = state + + def serialize_state(self) -> Dict[str, Dict[str, Any]]: + """格式不变""" + data = super().serialize_state() + data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) + return data + #移动极片前先取出对象 + def get_sheet_with_name(self, name: str) -> Optional[ElectrodeSheet]: + for sheet in self.children: + if sheet.name == name: + return sheet + return None + + def has_electrode_sheet(self) -> bool: + """检查洞位是否有极片""" + return len(self.children) > 0 + + def assign_child_resource( + self, + resource: ElectrodeSheet, + location: Optional[Coordinate], + reassign: bool = True, + ): + """放置极片""" + # TODO: 这里要改,diameter找不到,加入._unilabos_state后应该没问题 + if resource._unilabos_state["diameter"] > self._unilabos_state["diameter"]: + raise ValueError(f"极片直径 {resource._unilabos_state['diameter']} 超过洞位直径 {self._unilabos_state['diameter']}") + if len(self.children) >= self._unilabos_state["max_sheets"]: + raise ValueError(f"洞位已满,无法放置更多极片") + super().assign_child_resource(resource, location, reassign) + + # 根据children的编号取物料对象。 + def get_electrode_sheet_info(self, index: int) -> ElectrodeSheet: + return self.children[index] + + + +class MaterialPlateState(TypedDict): + hole_spacing_x: float + hole_spacing_y: float + hole_diameter: float + info: Optional[str] # 附加信息 + + + +class MaterialPlate(ItemizedResource[MaterialHole]): + """料板类 - 4x4个洞位,每个洞位放1个极片""" + + children: List[MaterialHole] + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + ordered_items: Optional[Dict[str, MaterialHole]] = None, + ordering: Optional[OrderedDict[str, str]] = None, + category: str = "material_plate", + model: Optional[str] = None, + fill: bool = False + ): + """初始化料板 + + Args: + name: 料板名称 + size_x: 长度 (mm) + size_y: 宽度 (mm) + size_z: 高度 (mm) + hole_diameter: 洞直径 (mm) + hole_depth: 洞深度 (mm) + hole_spacing_x: X方向洞位间距 (mm) + hole_spacing_y: Y方向洞位间距 (mm) + number: 编号 + category: 类别 + model: 型号 + """ + self._unilabos_state: MaterialPlateState = MaterialPlateState( + hole_spacing_x=24.0, + hole_spacing_y=24.0, + hole_diameter=20.0, + info="", + ) + # 创建4x4的洞位 + # TODO: 这里要改,对应不同形状 + holes = create_ordered_items_2d( + klass=MaterialHole, + num_items_x=4, + num_items_y=4, + dx=(size_x - 4 * self._unilabos_state["hole_spacing_x"]) / 2, # 居中 + dy=(size_y - 4 * self._unilabos_state["hole_spacing_y"]) / 2, # 居中 + dz=size_z, + item_dx=self._unilabos_state["hole_spacing_x"], + item_dy=self._unilabos_state["hole_spacing_y"], + size_x = 16, + size_y = 16, + size_z = 16, + ) + if fill: + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + ordered_items=holes, + category=category, + model=model, + ) + else: + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + ordered_items=ordered_items, + ordering=ordering, + category=category, + model=model, + ) + + def update_locations(self): + # TODO:调多次相加 + holes = create_ordered_items_2d( + klass=MaterialHole, + num_items_x=4, + num_items_y=4, + dx=(self._size_x - 3 * self._unilabos_state["hole_spacing_x"]) / 2, # 居中 + dy=(self._size_y - 3 * self._unilabos_state["hole_spacing_y"]) / 2, # 居中 + dz=self._size_z, + item_dx=self._unilabos_state["hole_spacing_x"], + item_dy=self._unilabos_state["hole_spacing_y"], + size_x = 1, + size_y = 1, + size_z = 1, + ) + for item, original_item in zip(holes.items(), self.children): + original_item.location = item[1].location + + +class PlateSlot(ResourceStack): + """板槽位类 - 1个槽上能堆放8个板,移板只能操作最上方的板""" + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + max_plates: int = 8, + category: str = "plate_slot", + model: Optional[str] = None + ): + """初始化板槽位 + + Args: + name: 槽位名称 + max_plates: 最大板数量 + category: 类别 + """ + super().__init__( + name=name, + direction="z", # Z方向堆叠 + resources=[], + ) + self.max_plates = max_plates + self.category = category + + def can_add_plate(self) -> bool: + """检查是否可以添加板""" + return len(self.children) < self.max_plates + + def add_plate(self, plate: MaterialPlate) -> None: + """添加料板""" + if not self.can_add_plate(): + raise ValueError(f"槽位 {self.name} 已满,无法添加更多板") + self.assign_child_resource(plate) + + def get_top_plate(self) -> MaterialPlate: + """获取最上方的板""" + if len(self.children) == 0: + raise ValueError(f"槽位 {self.name} 为空") + return cast(MaterialPlate, self.get_top_item()) + + def take_top_plate(self) -> MaterialPlate: + """取出最上方的板""" + top_plate = self.get_top_plate() + self.unassign_child_resource(top_plate) + return top_plate + + def can_access_for_picking(self) -> bool: + """检查是否可以进行取料操作(只有最上方的板能进行取料操作)""" + return len(self.children) > 0 + + def serialize(self) -> dict: + return { + **super().serialize(), + "max_plates": self.max_plates, + } + + +class ClipMagazineHole(Container): + """子弹夹洞位类""" + children: List[ElectrodeSheet] = [] + def __init__( + self, + name: str, + diameter: float, + depth: float, + category: str = "clip_magazine_hole", + ): + """初始化子弹夹洞位 + + Args: + name: 洞位名称 + diameter: 洞直径 (mm) + depth: 洞深度 (mm) + category: 类别 + """ + super().__init__( + name=name, + size_x=diameter, + size_y=diameter, + size_z=depth, + category=category, + ) + self.diameter = diameter + self.depth = depth + + def can_add_sheet(self, sheet: ElectrodeSheet) -> bool: + """检查是否可以添加极片 + + 根据洞的深度和极片的厚度来判断是否可以添加极片 + """ + # 检查极片直径是否适合洞的直径 + if sheet._unilabos_state["diameter"] > self.diameter: + return False + + # 计算当前已添加极片的总厚度 + current_thickness = sum(s._unilabos_state["thickness"] for s in self.children) + + # 检查添加新极片后总厚度是否超过洞的深度 + if current_thickness + sheet._unilabos_state["thickness"] > self.depth: + return False + + return True + + + def assign_child_resource( + self, + resource: ElectrodeSheet, + location: Optional[Coordinate] = None, + reassign: bool = True, + ): + """放置极片到洞位中 + + Args: + resource: 要放置的极片 + location: 极片在洞位中的位置(对于洞位,通常为None) + reassign: 是否允许重新分配 + """ + # 检查是否可以添加极片 + if not self.can_add_sheet(resource): + raise ValueError(f"无法向洞位 {self.name} 添加极片:直径或厚度不匹配") + + # 调用父类方法实际执行分配 + super().assign_child_resource(resource, location, reassign) + + def unassign_child_resource(self, resource: ElectrodeSheet): + """从洞位中移除极片 + + Args: + resource: 要移除的极片 + """ + if resource not in self.children: + raise ValueError(f"极片 {resource.name} 不在洞位 {self.name} 中") + + # 调用父类方法实际执行移除 + super().unassign_child_resource(resource) + + + + def serialize_state(self) -> Dict[str, Any]: + return { + "sheet_count": len(self.children), + "sheets": [sheet.serialize() for sheet in self.children], + } +class ClipMagazine_four(ItemizedResource[ClipMagazineHole]): + """子弹夹类 - 有4个洞位,每个洞位放多个极片""" + children: List[ClipMagazineHole] + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + hole_diameter: float = 14.0, + hole_depth: float = 10.0, + hole_spacing: float = 25.0, + max_sheets_per_hole: int = 100, + category: str = "clip_magazine_four", + model: Optional[str] = None, + ): + """初始化子弹夹 + + Args: + name: 子弹夹名称 + size_x: 长度 (mm) + size_y: 宽度 (mm) + size_z: 高度 (mm) + hole_diameter: 洞直径 (mm) + hole_depth: 洞深度 (mm) + hole_spacing: 洞位间距 (mm) + max_sheets_per_hole: 每个洞位最大极片数量 + category: 类别 + model: 型号 + """ + # 创建4个洞位,排成2x2布局 + holes = create_ordered_items_2d( + klass=ClipMagazineHole, + num_items_x=2, + num_items_y=2, + dx=(size_x - 2 * hole_spacing) / 2, # 居中 + dy=(size_y - hole_spacing) / 2, # 居中 + dz=size_z - 0, + item_dx=hole_spacing, + item_dy=hole_spacing, + diameter=hole_diameter, + depth=hole_depth, + ) + + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + ordered_items=holes, + category=category, + model=model, + ) + + # 保存洞位的直径和深度 + self.hole_diameter = hole_diameter + self.hole_depth = hole_depth + self.max_sheets_per_hole = max_sheets_per_hole + + def serialize(self) -> dict: + return { + **super().serialize(), + "hole_diameter": self.hole_diameter, + "hole_depth": self.hole_depth, + "max_sheets_per_hole": self.max_sheets_per_hole, + } +# TODO: 这个要改 +class ClipMagazine(ItemizedResource[ClipMagazineHole]): + """子弹夹类 - 有6个洞位,每个洞位放多个极片""" + children: List[ClipMagazineHole] + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + hole_diameter: float = 14.0, + hole_depth: float = 10.0, + hole_spacing: float = 25.0, + max_sheets_per_hole: int = 100, + category: str = "clip_magazine", + model: Optional[str] = None, + ): + """初始化子弹夹 + + Args: + name: 子弹夹名称 + size_x: 长度 (mm) + size_y: 宽度 (mm) + size_z: 高度 (mm) + hole_diameter: 洞直径 (mm) + hole_depth: 洞深度 (mm) + hole_spacing: 洞位间距 (mm) + max_sheets_per_hole: 每个洞位最大极片数量 + category: 类别 + model: 型号 + """ + # 创建6个洞位,排成2x3布局 + holes = create_ordered_items_2d( + klass=ClipMagazineHole, + num_items_x=3, + num_items_y=2, + dx=(size_x - 2 * hole_spacing) / 2, # 居中 + dy=(size_y - hole_spacing) / 2, # 居中 + dz=size_z - 0, + item_dx=hole_spacing, + item_dy=hole_spacing, + diameter=hole_diameter, + depth=hole_depth, + ) + + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + ordered_items=holes, + category=category, + model=model, + ) + + # 保存洞位的直径和深度 + self.hole_diameter = hole_diameter + self.hole_depth = hole_depth + self.max_sheets_per_hole = max_sheets_per_hole + + def serialize(self) -> dict: + return { + **super().serialize(), + "hole_diameter": self.hole_diameter, + "hole_depth": self.hole_depth, + "max_sheets_per_hole": self.max_sheets_per_hole, + } +#是一种类型注解,不用self +class BatteryState(TypedDict): + """电池状态字典""" + diameter: float + height: float + + electrolyte_name: str + electrolyte_volume: float + +class Battery(Resource): + """电池类 - 可容纳极片""" + children: List[ElectrodeSheet] = [] + + def __init__( + self, + name: str, + category: str = "battery", + ): + """初始化电池 + + Args: + name: 电池名称 + diameter: 直径 (mm) + height: 高度 (mm) + max_volume: 最大容量 (μL) + barcode: 二维码编号 + category: 类别 + model: 型号 + """ + super().__init__( + name=name, + size_x=1, + size_y=1, + size_z=1, + category=category, + ) + self._unilabos_state: BatteryState = BatteryState() + + def add_electrolyte_with_bottle(self, bottle: Bottle) -> bool: + to_add_name = bottle._unilabos_state["electrolyte_name"] + if bottle.aspirate_electrolyte(10): + if self.add_electrolyte(to_add_name, 10): + pass + else: + bottle._unilabos_state["electrolyte_volume"] += 10 + + def set_electrolyte(self, name: str, volume: float) -> None: + """设置电解液信息""" + self._unilabos_state["electrolyte_name"] = name + self._unilabos_state["electrolyte_volume"] = volume + #这个应该没用,不会有加了后再加的事情 + def add_electrolyte(self, name: str, volume: float) -> bool: + """添加电解液信息""" + if name != self._unilabos_state["electrolyte_name"]: + return False + self._unilabos_state["electrolyte_volume"] += volume + + def load_state(self, state: Dict[str, Any]) -> None: + """格式不变""" + super().load_state(state) + self._unilabos_state = state + + def serialize_state(self) -> Dict[str, Dict[str, Any]]: + """格式不变""" + data = super().serialize_state() + data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) + return data + +# 电解液作为属性放进去 + +class BatteryPressSlotState(TypedDict): + """电池状态字典""" + diameter: float =20.0 + depth: float = 4.0 + +class BatteryPressSlot(Resource): + """电池压制槽类 - 设备,可容纳一个电池""" + children: List[Battery] = [] + + def __init__( + self, + name: str = "BatteryPressSlot", + category: str = "battery_press_slot", + ): + """初始化电池压制槽 + + Args: + name: 压制槽名称 + diameter: 直径 (mm) + depth: 深度 (mm) + category: 类别 + model: 型号 + """ + super().__init__( + name=name, + size_x=10, + size_y=12, + size_z=13, + category=category, + ) + self._unilabos_state: BatteryPressSlotState = BatteryPressSlotState() + + def has_battery(self) -> bool: + """检查是否有电池""" + return len(self.children) > 0 + + def load_state(self, state: Dict[str, Any]) -> None: + """格式不变""" + super().load_state(state) + self._unilabos_state = state + + def serialize_state(self) -> Dict[str, Dict[str, Any]]: + """格式不变""" + data = super().serialize_state() + data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) + return data + + def assign_child_resource( + self, + resource: Battery, + location: Optional[Coordinate], + reassign: bool = True, + ): + """放置极片""" + # TODO: 让高京看下槽位只有一个电池时是否这么写。 + if self.has_battery(): + raise ValueError(f"槽位已含有一个电池,无法再放置其他电池") + super().assign_child_resource(resource, location, reassign) + + # 根据children的编号取物料对象。 + def get_battery_info(self, index: int) -> Battery: + return self.children[0] + +# TODO:这个移液枪架子看一下从哪继承 +class TipBox64State(TypedDict): + """电池状态字典""" + tip_diameter: float = 5.0 + tip_length: float = 50.0 + with_tips: bool = True + +class TipBox64(TipRack): + """64孔枪头盒类""" + + children: List[TipSpot] = [] + def __init__( + self, + name: str, + size_x: float = 127.8, + size_y: float = 85.5, + size_z: float = 60.0, + category: str = "tip_box_64", + model: Optional[str] = None, + ): + """初始化64孔枪头盒 + + Args: + name: 枪头盒名称 + size_x: 长度 (mm) + size_y: 宽度 (mm) + size_z: 高度 (mm) + tip_diameter: 枪头直径 (mm) + tip_length: 枪头长度 (mm) + category: 类别 + model: 型号 + with_tips: 是否带枪头 + """ + from pylabrobot.resources.tip import Tip + + # 创建8x8=64个枪头位 + def make_tip(): + return Tip( + has_filter=False, + total_tip_length=20.0, + maximal_volume=1000, # 1mL + fitting_depth=8.0, + ) + + tip_spots = create_ordered_items_2d( + klass=TipSpot, + num_items_x=8, + num_items_y=8, + dx=8.0, + dy=8.0, + dz=0.0, + item_dx=9.0, + item_dy=9.0, + size_x=10, + size_y=10, + size_z=0.0, + make_tip=make_tip, + ) + self._unilabos_state: WasteTipBoxstate = WasteTipBoxstate() + # 记录网格参数用于前端渲染 + self._grid_params = { + "num_items_x": 8, + "num_items_y": 8, + "dx": 8.0, + "dy": 8.0, + "item_dx": 9.0, + "item_dy": 9.0, + } + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + ordered_items=tip_spots, + category=category, + model=model, + with_tips=True, + ) + + def serialize(self) -> dict: + return { + **super().serialize(), + **self._grid_params, + } + + + +class WasteTipBoxstate(TypedDict): + """"废枪头盒状态字典""" + max_tips: int = 100 + tip_count: int = 0 + +#枪头不是一次性的(同一溶液则反复使用),根据寄存器判断 +class WasteTipBox(Trash): + """废枪头盒类 - 100个枪头容量""" + + def __init__( + self, + name: str, + size_x: float = 127.8, + size_y: float = 85.5, + size_z: float = 60.0, + category: str = "waste_tip_box", + model: Optional[str] = None, + ): + """初始化废枪头盒 + + Args: + name: 废枪头盒名称 + size_x: 长度 (mm) + size_y: 宽度 (mm) + size_z: 高度 (mm) + max_tips: 最大枪头容量 + category: 类别 + model: 型号 + """ + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + category=category, + model=model, + ) + self._unilabos_state: WasteTipBoxstate = WasteTipBoxstate() + + def add_tip(self) -> None: + """添加废枪头""" + if self._unilabos_state["tip_count"] >= self._unilabos_state["max_tips"]: + raise ValueError(f"废枪头盒 {self.name} 已满") + self._unilabos_state["tip_count"] += 1 + + def get_tip_count(self) -> int: + """获取枪头数量""" + return self._unilabos_state["tip_count"] + + def empty(self) -> None: + """清空废枪头盒""" + self._unilabos_state["tip_count"] = 0 + + + def load_state(self, state: Dict[str, Any]) -> None: + """格式不变""" + super().load_state(state) + self._unilabos_state = state + + def serialize_state(self) -> Dict[str, Dict[str, Any]]: + """格式不变""" + data = super().serialize_state() + data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) + return data + + +class BottleRackState(TypedDict): + """ bottle_diameter: 瓶子直径 (mm) + bottle_height: 瓶子高度 (mm) + position_spacing: 位置间距 (mm)""" + bottle_diameter: float + bottle_height: float + name_to_index: dict + + +class BottleRackState(TypedDict): + """ bottle_diameter: 瓶子直径 (mm) + bottle_height: 瓶子高度 (mm) + position_spacing: 位置间距 (mm)""" + bottle_diameter: float + bottle_height: float + position_spacing: float + name_to_index: dict + + +class BottleRack(Resource): + """瓶架类 - 12个待配位置+12个已配位置""" + children: List[Resource] = [] + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + category: str = "bottle_rack", + model: Optional[str] = None, + num_items_x: int = 3, + num_items_y: int = 4, + position_spacing: float = 35.0, + orientation: str = "horizontal", + padding_x: float = 20.0, + padding_y: float = 20.0, + ): + """初始化瓶架 + + Args: + name: 瓶架名称 + size_x: 长度 (mm) + size_y: 宽度 (mm) + size_z: 高度 (mm) + category: 类别 + model: 型号 + """ + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + category=category, + model=model, + ) + # 初始化状态 + self._unilabos_state: BottleRackState = BottleRackState( + bottle_diameter=30.0, + bottle_height=100.0, + position_spacing=position_spacing, + name_to_index={}, + ) + # 基于网格生成瓶位坐标映射(居中摆放) + # 使用内边距,避免点跑到容器外(前端渲染不按mm等比缩放时更稳妥) + origin_x = padding_x + origin_y = padding_y + self.index_to_pos = {} + for j in range(num_items_y): + for i in range(num_items_x): + idx = j * num_items_x + i + if orientation == "vertical": + # 纵向:沿 y 方向优先排列 + self.index_to_pos[idx] = Coordinate( + x=origin_x + j * position_spacing, + y=origin_y + i * position_spacing, + z=0, + ) + else: + # 横向(默认):沿 x 方向优先排列 + self.index_to_pos[idx] = Coordinate( + x=origin_x + i * position_spacing, + y=origin_y + j * position_spacing, + z=0, + ) + self.name_to_index = {} + self.name_to_pos = {} + self.num_items_x = num_items_x + self.num_items_y = num_items_y + self.orientation = orientation + self.padding_x = padding_x + self.padding_y = padding_y + + def load_state(self, state: Dict[str, Any]) -> None: + """格式不变""" + super().load_state(state) + self._unilabos_state = state + + def serialize_state(self) -> Dict[str, Dict[str, Any]]: + """格式不变""" + data = super().serialize_state() + data.update( + self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) + return data + + # TODO: 这里有些问题要重新写一下 + def assign_child_resource_old(self, resource: Resource, location=Coordinate.zero(), reassign=True): + capacity = self.num_items_x * self.num_items_y + assert len(self.children) < capacity, "瓶架已满,无法添加更多瓶子" + index = len(self.children) + location = self.index_to_pos.get(index, Coordinate.zero()) + self.name_to_pos[resource.name] = location + self.name_to_index[resource.name] = index + return super().assign_child_resource(resource, location, reassign) + + def assign_child_resource(self, resource: Resource, index: int): + capacity = self.num_items_x * self.num_items_y + assert 0 <= index < capacity, "无效的瓶子索引" + self.name_to_index[resource.name] = index + location = self.index_to_pos[index] + return super().assign_child_resource(resource, location) + + def unassign_child_resource(self, resource: Bottle): + super().unassign_child_resource(resource) + self.index_to_pos.pop(self.name_to_index.pop(resource.name, None), None) + + def serialize(self) -> dict: + return { + **super().serialize(), + "num_items_x": self.num_items_x, + "num_items_y": self.num_items_y, + "position_spacing": self._unilabos_state.get("position_spacing", 35.0), + "orientation": self.orientation, + "padding_x": self.padding_x, + "padding_y": self.padding_y, + } + + +class BottleState(TypedDict): + diameter: float + height: float + electrolyte_name: str + electrolyte_volume: float + max_volume: float + +class Bottle(Resource): + """瓶子类 - 容纳电解液""" + + def __init__( + self, + name: str, + category: str = "bottle", + ): + """初始化瓶子 + + Args: + name: 瓶子名称 + diameter: 直径 (mm) + height: 高度 (mm) + max_volume: 最大体积 (μL) + barcode: 二维码 + category: 类别 + model: 型号 + """ + super().__init__( + name=name, + size_x=1, + size_y=1, + size_z=1, + category=category, + ) + self._unilabos_state: BottleState = BottleState() + + def aspirate_electrolyte(self, volume: float) -> bool: + current_volume = self._unilabos_state["electrolyte_volume"] + assert current_volume > volume, f"Cannot aspirate {volume}μL, only {current_volume}μL available." + self._unilabos_state["electrolyte_volume"] -= volume + return True + + def load_state(self, state: Dict[str, Any]) -> None: + """格式不变""" + super().load_state(state) + self._unilabos_state = state + + def serialize_state(self) -> Dict[str, Dict[str, Any]]: + """格式不变""" + data = super().serialize_state() + data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) + return data + +class CoincellDeck(Deck): + """纽扣电池组装工作站台面类""" + + def __init__( + self, + name: str = "coin_cell_deck", + size_x: float = 1620.0, # 3.66m + size_y: float = 1270.0, # 1.23m + size_z: float = 500.0, + origin: Coordinate = Coordinate(0, 0, 0), + category: str = "coin_cell_deck", + ): + """初始化纽扣电池组装工作站台面 + + Args: + name: 台面名称 + size_x: 长度 (mm) - 3.66m + size_y: 宽度 (mm) - 1.23m + size_z: 高度 (mm) + origin: 原点坐标 + category: 类别 + """ + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + origin=origin, + category=category, + ) + +#if __name__ == "__main__": +# # 转移极片的测试代码 +# deck = CoincellDeck("coin_cell_deck") +# ban_cao_wei = PlateSlot("ban_cao_wei", max_plates=8) +# deck.assign_child_resource(ban_cao_wei, Coordinate(x=0, y=0, z=0)) +# +# plate_1 = MaterialPlate("plate_1", 1,1,1, fill=True) +# for i, hole in enumerate(plate_1.children): +# sheet = ElectrodeSheet(f"hole_{i}_sheet_1") +# sheet._unilabos_state = { +# "diameter": 14, +# "info": "NMC", +# "mass": 5.0, +# "material_type": "positive_electrode", +# "thickness": 0.1 +# } +# hole._unilabos_state = { +# "depth": 1.0, +# "diameter": 14, +# "info": "", +# "max_sheets": 1 +# } +# hole.assign_child_resource(sheet, Coordinate.zero()) +# plate_1._unilabos_state = { +# "hole_spacing_x": 20.0, +# "hole_spacing_y": 20.0, +# "hole_diameter": 5, +# "info": "这是第一块料板" +# } +# plate_1.update_locations() +# ban_cao_wei.assign_child_resource(plate_1, Coordinate.zero()) +# # zi_dan_jia = ClipMagazine("zi_dan_jia", 1, 1, 1) +# # deck.assign_child_resource(ban_cao_wei, Coordinate(x=200, y=200, z=0)) +# +# from unilabos.resources.graphio import * +# A = tree_to_list([resource_plr_to_ulab(deck)]) +# with open("test.json", "w") as f: +# json.dump(A, f) +# +# +#def get_plate_with_14mm_hole(name=""): +# plate = MaterialPlate(name=name) +# for i in range(4): +# for j in range(4): +# hole = MaterialHole(f"{i+1}x{j+1}") +# hole._unilabos_state["diameter"] = 14 +# hole._unilabos_state["max_sheets"] = 1 +# plate.assign_child_resource(hole) +# return plate + +def create_a_liaopan(): + liaopan = MaterialPlate(name="liaopan", size_x=120.8, size_y=120.5, size_z=10.0, fill=True) + for i in range(16): + jipian = ElectrodeSheet(name=f"jipian_{i}", size_x= 12, size_y=12, size_z=0.1) + liaopan1.children[i].assign_child_resource(jipian, location=None) + return liaopan + +def create_a_coin_cell_deck(): + deck = Deck(size_x=1200, + size_y=800, + size_z=900) + + #liaopan = TipBox64(name="liaopan") + + #创建一个4*4的物料板 + liaopan1 = MaterialPlate(name="liaopan1", size_x=120.8, size_y=120.5, size_z=10.0, fill=True) + #把物料板放到桌子上 + deck.assign_child_resource(liaopan1, Coordinate(x=0, y=0, z=0)) + #创建一个极片 + for i in range(16): + jipian = ElectrodeSheet(name=f"jipian_{i}", size_x= 12, size_y=12, size_z=0.1) + liaopan1.children[i].assign_child_resource(jipian, location=None) + #创建一个4*4的物料板 + liaopan2 = MaterialPlate(name="liaopan2", size_x=120.8, size_y=120.5, size_z=10.0, fill=True) + #把物料板放到桌子上 + deck.assign_child_resource(liaopan2, Coordinate(x=500, y=0, z=0)) + + #创建一个4*4的物料板 + liaopan3 = MaterialPlate(name="liaopan3", size_x=120.8, size_y=120.5, size_z=10.0, fill=True) + #把物料板放到桌子上 + deck.assign_child_resource(liaopan3, Coordinate(x=1000, y=0, z=0)) + + print(deck) + + return deck + + +import json + +if __name__ == "__main__": + electrode1 = BatteryPressSlot() + #print(electrode1.get_size_x()) + #print(electrode1.get_size_y()) + #print(electrode1.get_size_z()) + #jipian = ElectrodeSheet() + #jipian._unilabos_state["diameter"] = 18 + #print(jipian.serialize()) + #print(jipian.serialize_state()) + + deck = CoincellDeck() + """======================================子弹夹============================================""" + zip_dan_jia = ClipMagazine_four("zi_dan_jia", 80, 80, 10) + deck.assign_child_resource(zip_dan_jia, Coordinate(x=1400, y=50, z=0)) + zip_dan_jia2 = ClipMagazine_four("zi_dan_jia2", 80, 80, 10) + deck.assign_child_resource(zip_dan_jia2, Coordinate(x=1600, y=200, z=0)) + zip_dan_jia3 = ClipMagazine("zi_dan_jia3", 80, 80, 10) + deck.assign_child_resource(zip_dan_jia3, Coordinate(x=1500, y=200, z=0)) + zip_dan_jia4 = ClipMagazine("zi_dan_jia4", 80, 80, 10) + deck.assign_child_resource(zip_dan_jia4, Coordinate(x=1500, y=300, z=0)) + zip_dan_jia5 = ClipMagazine("zi_dan_jia5", 80, 80, 10) + deck.assign_child_resource(zip_dan_jia5, Coordinate(x=1600, y=300, z=0)) + zip_dan_jia6 = ClipMagazine("zi_dan_jia6", 80, 80, 10) + deck.assign_child_resource(zip_dan_jia6, Coordinate(x=1530, y=500, z=0)) + zip_dan_jia7 = ClipMagazine("zi_dan_jia7", 80, 80, 10) + deck.assign_child_resource(zip_dan_jia7, Coordinate(x=1180, y=400, z=0)) + zip_dan_jia8 = ClipMagazine("zi_dan_jia8", 80, 80, 10) + deck.assign_child_resource(zip_dan_jia8, Coordinate(x=1280, y=400, z=0)) + for i in range(4): + jipian = ElectrodeSheet(name=f"zi_dan_jia_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + zip_dan_jia2.children[i].assign_child_resource(jipian, location=None) + for i in range(4): + jipian2 = ElectrodeSheet(name=f"zi_dan_jia2_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + zip_dan_jia.children[i].assign_child_resource(jipian2, location=None) + for i in range(6): + jipian3 = ElectrodeSheet(name=f"zi_dan_jia3_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + zip_dan_jia3.children[i].assign_child_resource(jipian3, location=None) + for i in range(6): + jipian4 = ElectrodeSheet(name=f"zi_dan_jia4_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + zip_dan_jia4.children[i].assign_child_resource(jipian4, location=None) + for i in range(6): + jipian5 = ElectrodeSheet(name=f"zi_dan_jia5_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + zip_dan_jia5.children[i].assign_child_resource(jipian5, location=None) + for i in range(6): + jipian6 = ElectrodeSheet(name=f"zi_dan_jia6_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + zip_dan_jia6.children[i].assign_child_resource(jipian6, location=None) + for i in range(6): + jipian7 = ElectrodeSheet(name=f"zi_dan_jia7_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + zip_dan_jia7.children[i].assign_child_resource(jipian7, location=None) + for i in range(6): + jipian8 = ElectrodeSheet(name=f"zi_dan_jia8_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + zip_dan_jia8.children[i].assign_child_resource(jipian8, location=None) + """======================================子弹夹============================================""" + #liaopan = TipBox64(name="liaopan") + """======================================物料板============================================""" + #创建一个4*4的物料板 + liaopan1 = MaterialPlate(name="liaopan1", size_x=120, size_y=100, size_z=10.0, fill=True) + deck.assign_child_resource(liaopan1, Coordinate(x=1010, y=50, z=0)) + for i in range(16): + jipian_1 = ElectrodeSheet(name=f"{liaopan1.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + liaopan1.children[i].assign_child_resource(jipian_1, location=None) + + liaopan2 = MaterialPlate(name="liaopan2", size_x=120, size_y=100, size_z=10.0, fill=True) + deck.assign_child_resource(liaopan2, Coordinate(x=1130, y=50, z=0)) + + liaopan3 = MaterialPlate(name="liaopan3", size_x=120, size_y=100, size_z=10.0, fill=True) + deck.assign_child_resource(liaopan3, Coordinate(x=1250, y=50, z=0)) + + liaopan4 = MaterialPlate(name="liaopan4", size_x=120, size_y=100, size_z=10.0, fill=True) + deck.assign_child_resource(liaopan4, Coordinate(x=1010, y=150, z=0)) + for i in range(16): + jipian_4 = ElectrodeSheet(name=f"{liaopan4.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + liaopan4.children[i].assign_child_resource(jipian_4, location=None) + liaopan5 = MaterialPlate(name="liaopan5", size_x=120, size_y=100, size_z=10.0, fill=True) + deck.assign_child_resource(liaopan5, Coordinate(x=1130, y=150, z=0)) + liaopan6 = MaterialPlate(name="liaopan6", size_x=120, size_y=100, size_z=10.0, fill=True) + deck.assign_child_resource(liaopan6, Coordinate(x=1250, y=150, z=0)) + #liaopan.children[3].assign_child_resource(jipian, location=None) + """======================================物料板============================================""" + """======================================瓶架,移液枪============================================""" + # 在台面上放置 3x4 瓶架、6x2 瓶架 与 64孔移液枪头盒 + bottle_rack_3x4 = BottleRack( + name="bottle_rack_3x4", + size_x=210.0, + size_y=140.0, + size_z=100.0, + num_items_x=3, + num_items_y=4, + position_spacing=35.0, + orientation="vertical", + ) + deck.assign_child_resource(bottle_rack_3x4, Coordinate(x=100, y=200, z=0)) + + bottle_rack_6x2 = BottleRack( + name="bottle_rack_6x2", + size_x=120.0, + size_y=250.0, + size_z=100.0, + num_items_x=6, + num_items_y=2, + position_spacing=35.0, + orientation="vertical", + ) + deck.assign_child_resource(bottle_rack_6x2, Coordinate(x=300, y=300, z=0)) + + bottle_rack_6x2_2 = BottleRack( + name="bottle_rack_6x2_2", + size_x=120.0, + size_y=250.0, + size_z=100.0, + num_items_x=6, + num_items_y=2, + position_spacing=35.0, + orientation="vertical", + ) + deck.assign_child_resource(bottle_rack_6x2_2, Coordinate(x=430, y=300, z=0)) + + + # 将 ElectrodeSheet 放满 3x4 与 6x2 的所有孔位 + for idx in range(bottle_rack_3x4.num_items_x * bottle_rack_3x4.num_items_y): + sheet = ElectrodeSheet(name=f"sheet_3x4_{idx}", size_x=12, size_y=12, size_z=0.1) + bottle_rack_3x4.assign_child_resource(sheet, index=idx) + + for idx in range(bottle_rack_6x2.num_items_x * bottle_rack_6x2.num_items_y): + sheet = ElectrodeSheet(name=f"sheet_6x2_{idx}", size_x=12, size_y=12, size_z=0.1) + bottle_rack_6x2.assign_child_resource(sheet, index=idx) + + tip_box = TipBox64(name="tip_box_64") + deck.assign_child_resource(tip_box, Coordinate(x=300, y=100, z=0)) + + waste_tip_box = WasteTipBox(name="waste_tip_box") + deck.assign_child_resource(waste_tip_box, Coordinate(x=300, y=200, z=0)) + """======================================瓶架,移液枪============================================""" + print(deck) + + + from unilabos.resources.graphio import convert_resources_from_type + from unilabos.config.config import BasicConfig + BasicConfig.ak = "56bbed5b-6e30-438c-b06d-f69eaa63bb45" + BasicConfig.sk = "238222fe-0bf7-4350-a426-e5ced8011dcf" + from unilabos.app.web.client import http_client + + resources = convert_resources_from_type([deck], [Resource]) + + # 检查序列化后的资源 + + json.dump({"nodes": resources, "links": []}, open("button_battery_station_resources_unilab.json", "w"), indent=2) + + + #print(resources) + http_client.remote_addr = "https://uni-lab.test.bohrium.com/api/v1" + + http_client.resource_add(resources) \ No newline at end of file diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py index ee88e602..1fefd8ce 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py @@ -1,39 +1,1142 @@ +import csv +import json +import os +import threading +import time +from datetime import datetime from typing import Any, Dict, Optional from pylabrobot.resources import Resource as PLRResource +from unilabos_msgs.msg import Resource from unilabos.device_comms.modbus_plc.client import ModbusTcpClient -from unilabos.devices.workstation.workstation_base import ResourceSynchronizer, WorkstationBase +from unilabos.devices.workstation.coin_cell_assembly.button_battery_station import MaterialHole, MaterialPlate +from unilabos.devices.workstation.workstation_base import WorkstationBase +from unilabos.device_comms.modbus_plc.client import TCPClient, ModbusNode, PLCWorkflow, ModbusWorkflow, WorkflowAction, BaseClient +from unilabos.device_comms.modbus_plc.modbus import DeviceType, Base as ModbusNodeBase, DataType, WorderOrder +from unilabos.devices.workstation.coin_cell_assembly.button_battery_station import * +from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode +from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode +#构建物料系统 class CoinCellAssemblyWorkstation(WorkstationBase): def __init__( self, - device_id: str, - deck_config: Dict[str, Any], - children: Optional[Dict[str, Any]] = None, - resource_synchronizer: Optional[ResourceSynchronizer] = None, - host: str = "192.168.0.0", - port: str = "", + station_resource: CoincellDeck, + address: str = "192.168.1.20", + port: str = "502", + debug_mode: bool = True, *args, **kwargs, ): super().__init__( - device_id=device_id, - deck_config=deck_config, - children=children, - resource_synchronizer=resource_synchronizer, + #桌子 + station_resource=station_resource, *args, **kwargs, ) + self.debug_mode = debug_mode + self.station_resource = station_resource + """ 连接初始化 """ + modbus_client = TCPClient(addr=address, port=port) + print("modbus_client", modbus_client) + if not debug_mode: + modbus_client.client.connect() + count = 100 + while count >0: + count -=1 + if modbus_client.client.is_socket_open(): + break + time.sleep(2) + if not modbus_client.client.is_socket_open(): + raise ValueError('modbus tcp connection failed') + else: + print("测试模式,跳过连接") + + """ 工站的配置 """ + self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_a.csv')) + self.client = modbus_client.register_node_list(self.nodes) + self.success = False + self.allow_data_read = False #允许读取函数运行标志位 + self.csv_export_thread = None + self.csv_export_running = False + self.csv_export_file = None + #创建一个物料台面,包含两个极片板 + #self.deck = create_a_coin_cell_deck() - self.hardware_interface = ModbusTcpClient(host=host, port=port) - - def run_assembly(self, wf_name: str, resource: PLRResource, params: str = "\{\}"): - """启动工作流""" - self.current_workflow_status = WorkflowStatus.RUNNING - logger.info(f"工作站 {self.device_id} 启动工作流: {wf_name}") - - # TODO: 实现工作流逻辑 - - anode_sheet = self.deck.get_resource("anode_sheet") + #self._ros_node.update_resource(self.deck) - \ No newline at end of file + #ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ + # "resources": [self.deck] + #}) + + + def post_init(self, ros_node: ROS2WorkstationNode): + self._ros_node = ros_node + #self.deck = create_a_coin_cell_deck() + ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ + "resources": [self.station_resource] + }) + + # 批量操作在这里写 + async def change_hole_sheet_to_2(self, hole: MaterialHole): + hole._unilabos_state["max_sheets"] = 2 + return await self._ros_node.update_resource(hole) + + + async def fill_plate(self): + plate_1: MaterialPlate = self.station_resource.children[0].children[0] + #plate_1 + return await self._ros_node.update_resource(plate_1) + + #def run_assembly(self, wf_name: str, resource: PLRResource, params: str = "\{\}"): + # """启动工作流""" + # self.current_workflow_status = WorkflowStatus.RUNNING + # logger.info(f"工作站 {self.device_id} 启动工作流: {wf_name}") +# + # # TODO: 实现工作流逻辑 +# + # anode_sheet = self.deck.get_resource("anode_sheet") + + """ Action逻辑代码 """ + def _sys_start_cmd(self, cmd=None): + """设备启动命令 (可读写)""" + if cmd is not None: # 写入模式 + self.success = False + node = self.client.use_node('COIL_SYS_START_CMD') + ret = node.write(cmd) + print(ret) + self.success = True + return self.success + else: # 读取模式 + cmd_feedback, read_err = self.client.use_node('COIL_SYS_START_CMD').read(1) + return cmd_feedback[0] + + def _sys_stop_cmd(self, cmd=None): + """设备停止命令 (可读写)""" + if cmd is not None: # 写入模式 + self.success = False + node = self.client.use_node('COIL_SYS_STOP_CMD') + node.write(cmd) + self.success = True + return self.success + else: # 读取模式 + cmd_feedback, read_err = self.client.use_node('COIL_SYS_STOP_CMD').read(1) + return cmd_feedback[0] + + def _sys_reset_cmd(self, cmd=None): + """设备复位命令 (可读写)""" + if cmd is not None: + self.success = False + self.client.use_node('COIL_SYS_RESET_CMD').write(cmd) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('COIL_SYS_RESET_CMD').read(1) + return cmd_feedback[0] + + def _sys_hand_cmd(self, cmd=None): + """手动模式命令 (可读写)""" + if cmd is not None: + self.success = False + self.client.use_node('COIL_SYS_HAND_CMD').write(cmd) + self.success = True + print("步骤0") + return self.success + else: + cmd_feedback, read_err = self.client.use_node('COIL_SYS_HAND_CMD').read(1) + return cmd_feedback[0] + + def _sys_auto_cmd(self, cmd=None): + """自动模式命令 (可读写)""" + if cmd is not None: + self.success = False + self.client.use_node('COIL_SYS_AUTO_CMD').write(cmd) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('COIL_SYS_AUTO_CMD').read(1) + return cmd_feedback[0] + + def _sys_init_cmd(self, cmd=None): + """初始化命令 (可读写)""" + if cmd is not None: + self.success = False + self.client.use_node('COIL_SYS_INIT_CMD').write(cmd) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('COIL_SYS_INIT_CMD').read(1) + return cmd_feedback[0] + + def _unilab_send_msg_succ_cmd(self, cmd=None): + """UNILAB发送配方完毕 (可读写)""" + if cmd is not None: + self.success = False + self.client.use_node('COIL_UNILAB_SEND_MSG_SUCC_CMD').write(cmd) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('COIL_UNILAB_SEND_MSG_SUCC_CMD').read(1) + return cmd_feedback[0] + + def _unilab_rec_msg_succ_cmd(self, cmd=None): + """UNILAB接收测试电池数据完毕 (可读写)""" + if cmd is not None: + self.success = False + self.client.use_node('COIL_UNILAB_REC_MSG_SUCC_CMD').write(cmd) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('COIL_UNILAB_REC_MSG_SUCC_CMD').read(1) + return cmd_feedback + + + # ====================== 命令类指令(REG_x_) ====================== + def _unilab_send_msg_electrolyte_num(self, num=None): + """UNILAB写电解液使用瓶数(可读写)""" + if num is not None: + self.success = False + ret = self.client.use_node('REG_MSG_ELECTROLYTE_NUM').write(num) + print(ret) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('REG_MSG_ELECTROLYTE_NUM').read(1) + return cmd_feedback[0] + + def _unilab_send_msg_electrolyte_use_num(self, use_num=None): + """UNILAB写单次电解液使用瓶数(可读写)""" + if use_num is not None: + self.success = False + self.client.use_node('REG_MSG_ELECTROLYTE_USE_NUM').write(use_num) + self.success = True + return self.success + else: + return False + + def _unilab_send_msg_assembly_type(self, num=None): + """UNILAB写组装参数""" + if num is not None: + self.success = False + self.client.use_node('REG_MSG_ASSEMBLY_TYPE').write(num) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('REG_MSG_ASSEMBLY_TYPE').read(1) + return cmd_feedback[0] + + def _unilab_send_msg_electrolyte_vol(self, vol=None): + """UNILAB写电解液吸取量参数""" + if vol is not None: + self.success = False + self.client.use_node('REG_MSG_ELECTROLYTE_VOLUME').write(vol, data_type=DataType.FLOAT32, word_order=WorderOrder.LITTLE) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('REG_MSG_ELECTROLYTE_VOLUME').read(2, word_order=WorderOrder.LITTLE) + return cmd_feedback[0] + + def _unilab_send_msg_assembly_pressure(self, vol=None): + """UNILAB写电池压制力""" + if vol is not None: + self.success = False + self.client.use_node('REG_MSG_ASSEMBLY_PRESSURE').write(vol, data_type=DataType.FLOAT32, word_order=WorderOrder.LITTLE) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('REG_MSG_ASSEMBLY_PRESSURE').read(2, word_order=WorderOrder.LITTLE) + return cmd_feedback[0] + + # ==================== 0905新增内容(COIL_x_STATUS) ==================== + def _unilab_send_electrolyte_bottle_num(self, num=None): + """UNILAB发送电解液瓶数完毕""" + if num is not None: + self.success = False + self.client.use_node('UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM').write(num) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM').read(1) + return cmd_feedback[0] + + def _unilab_rece_electrolyte_bottle_num(self, num=None): + """设备请求接受电解液瓶数""" + if num is not None: + self.success = False + self.client.use_node('UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM').write(num) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM').read(1) + return cmd_feedback[0] + + def _reg_msg_electrolyte_num(self, num=None): + """电解液已使用瓶数""" + if num is not None: + self.success = False + self.client.use_node('REG_MSG_ELECTROLYTE_NUM').write(num) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('REG_MSG_ELECTROLYTE_NUM').read(1) + return cmd_feedback[0] + + def _reg_data_electrolyte_use_num(self, num=None): + """单瓶电解液完成组装数""" + if num is not None: + self.success = False + self.client.use_node('REG_DATA_ELECTROLYTE_USE_NUM').write(num) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('REG_DATA_ELECTROLYTE_USE_NUM').read(1) + return cmd_feedback[0] + + def _unilab_send_finished_cmd(self, num=None): + """Unilab发送已知一组组装完成信号""" + if num is not None: + self.success = False + self.client.use_node('UNILAB_SEND_FINISHED_CMD').write(num) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('UNILAB_SEND_FINISHED_CMD').read(1) + return cmd_feedback[0] + + def _unilab_rece_finished_cmd(self, num=None): + """Unilab接收已知一组组装完成信号""" + if num is not None: + self.success = False + self.client.use_node('UNILAB_RECE_FINISHED_CMD').write(num) + self.success = True + return self.success + else: + cmd_feedback, read_err = self.client.use_node('UNILAB_RECE_FINISHED_CMD').read(1) + return cmd_feedback[0] + + + + # ==================== 状态类属性(COIL_x_STATUS) ==================== + def _sys_start_status(self) -> bool: + """设备启动中( BOOL)""" + status, read_err = self.client.use_node('COIL_SYS_START_STATUS').read(1) + return status[0] + + def _sys_stop_status(self) -> bool: + """设备停止中( BOOL)""" + status, read_err = self.client.use_node('COIL_SYS_STOP_STATUS').read(1) + return status[0] + + def _sys_reset_status(self) -> bool: + """设备复位中( BOOL)""" + status, read_err = self.client.use_node('COIL_SYS_RESET_STATUS').read(1) + return status[0] + + def _sys_init_status(self) -> bool: + """设备初始化完成( BOOL)""" + status, read_err = self.client.use_node('COIL_SYS_INIT_STATUS').read(1) + return status[0] + + # 查找资源 + def modify_deck_name(self, resource_name: str): + # figure_res = self._ros_node.resource_tracker.figure_resource({"name": resource_name}) + # print(f"!!! figure_res: {type(figure_res)}") + self.station_resource.children[1] + return + + @property + def sys_status(self) -> str: + if self.debug_mode: + return "设备调试模式" + if self._sys_start_status(): + return "设备启动中" + elif self._sys_stop_status(): + return "设备停止中" + elif self._sys_reset_status(): + return "设备复位中" + elif self._sys_init_status(): + return "设备初始化中" + else: + return "未知状态" + + def _sys_hand_status(self) -> bool: + """设备手动模式( BOOL)""" + status, read_err = self.client.use_node('COIL_SYS_HAND_STATUS').read(1) + return status[0] + + def _sys_auto_status(self) -> bool: + """设备自动模式( BOOL)""" + status, read_err = self.client.use_node('COIL_SYS_AUTO_STATUS').read(1) + return status[0] + + @property + def sys_mode(self) -> str: + if self.debug_mode: + return "设备调试模式" + if self._sys_hand_status(): + return "设备手动模式" + elif self._sys_auto_status(): + return "设备自动模式" + else: + return "未知模式" + + @property + def request_rec_msg_status(self) -> bool: + """设备请求接受配方( BOOL)""" + if self.debug_mode: + return True + status, read_err = self.client.use_node('COIL_REQUEST_REC_MSG_STATUS').read(1) + return status[0] + + @property + def request_send_msg_status(self) -> bool: + """设备请求发送测试数据( BOOL)""" + if self.debug_mode: + return True + status, read_err = self.client.use_node('COIL_REQUEST_SEND_MSG_STATUS').read(1) + return status[0] + + # ======================= 其他属性(特殊功能) ======================== + ''' + @property + def warning_1(self) -> bool: + status, read_err = self.client.use_node('COIL_WARNING_1').read(1) + return status[0] + ''' + # ===================== 生产数据区 ====================== + + @property + def data_assembly_coin_cell_num(self) -> int: + """已完成电池数量 (INT16)""" + if self.debug_mode: + return 0 + num, read_err = self.client.use_node('REG_DATA_ASSEMBLY_COIN_CELL_NUM').read(1) + return num + + @property + def data_assembly_time(self) -> float: + """单颗电池组装时间 (秒, REAL/FLOAT32)""" + if self.debug_mode: + return 0 + time, read_err = self.client.use_node('REG_DATA_ASSEMBLY_PER_TIME').read(2, word_order=WorderOrder.LITTLE) + return time + + @property + def data_open_circuit_voltage(self) -> float: + """开路电压值 (FLOAT32)""" + if self.debug_mode: + return 0 + vol, read_err = self.client.use_node('REG_DATA_OPEN_CIRCUIT_VOLTAGE').read(2, word_order=WorderOrder.LITTLE) + return vol + + @property + def data_axis_x_pos(self) -> float: + """分液X轴当前位置 (FLOAT32)""" + if self.debug_mode: + return 0 + pos, read_err = self.client.use_node('REG_DATA_AXIS_X_POS').read(2, word_order=WorderOrder.LITTLE) + return pos + + @property + def data_axis_y_pos(self) -> float: + """分液Y轴当前位置 (FLOAT32)""" + if self.debug_mode: + return 0 + pos, read_err = self.client.use_node('REG_DATA_AXIS_Y_POS').read(2, word_order=WorderOrder.LITTLE) + return pos + + @property + def data_axis_z_pos(self) -> float: + """分液Z轴当前位置 (FLOAT32)""" + if self.debug_mode: + return 0 + pos, read_err = self.client.use_node('REG_DATA_AXIS_Z_POS').read(2, word_order=WorderOrder.LITTLE) + return pos + + @property + def data_pole_weight(self) -> float: + """当前电池正极片称重数据 (FLOAT32)""" + if self.debug_mode: + return 0 + weight, read_err = self.client.use_node('REG_DATA_POLE_WEIGHT').read(2, word_order=WorderOrder.LITTLE) + return weight + + @property + def data_assembly_pressure(self) -> int: + """当前电池压制力 (INT16)""" + if self.debug_mode: + return 0 + pressure, read_err = self.client.use_node('REG_DATA_ASSEMBLY_PRESSURE').read(1) + return pressure + + @property + def data_electrolyte_volume(self) -> int: + """当前电解液加注量 (INT16)""" + if self.debug_mode: + return 0 + vol, read_err = self.client.use_node('REG_DATA_ELECTROLYTE_VOLUME').read(1) + return vol + + @property + def data_coin_num(self) -> int: + """当前电池数量 (INT16)""" + if self.debug_mode: + return 0 + num, read_err = self.client.use_node('REG_DATA_COIN_NUM').read(1) + return num + + @property + def data_coin_cell_code(self) -> str: + """电池二维码序列号 (STRING)""" + try: + # 尝试不同的字节序读取 + code_little, read_err = self.client.use_node('REG_DATA_COIN_CELL_CODE').read(10, word_order=WorderOrder.LITTLE) + print(code_little) + clean_code = code_little[-8:][::-1] + return clean_code + except Exception as e: + print(f"读取电池二维码失败: {e}") + return "N/A" + + + @property + def data_electrolyte_code(self) -> str: + try: + # 尝试不同的字节序读取 + code_little, read_err = self.client.use_node('REG_DATA_ELECTROLYTE_CODE').read(10, word_order=WorderOrder.LITTLE) + print(code_little) + clean_code = code_little[-8:][::-1] + return clean_code + except Exception as e: + print(f"读取电解液二维码失败: {e}") + return "N/A" + + # ===================== 环境监控区 ====================== + @property + def data_glove_box_pressure(self) -> float: + """手套箱压力 (bar, FLOAT32)""" + if self.debug_mode: + return 0 + status, read_err = self.client.use_node('REG_DATA_GLOVE_BOX_PRESSURE').read(2, word_order=WorderOrder.LITTLE) + return status + + @property + def data_glove_box_o2_content(self) -> float: + """手套箱氧含量 (ppm, FLOAT32)""" + if self.debug_mode: + return 0 + value, read_err = self.client.use_node('REG_DATA_GLOVE_BOX_O2_CONTENT').read(2, word_order=WorderOrder.LITTLE) + return value + + @property + def data_glove_box_water_content(self) -> float: + """手套箱水含量 (ppm, FLOAT32)""" + if self.debug_mode: + return 0 + value, read_err = self.client.use_node('REG_DATA_GLOVE_BOX_WATER_CONTENT').read(2, word_order=WorderOrder.LITTLE) + return value + +# @property +# def data_stack_vision_code(self) -> int: +# """物料堆叠复检图片编码 (INT16)""" +# if self.debug_mode: +# return 0 +# code, read_err = self.client.use_node('REG_DATA_STACK_VISON_CODE').read(1) +# #code, _ = self.client.use_node('REG_DATA_STACK_VISON_CODE').read(1).type +# print(f"读取物料堆叠复检图片编码", {code}, "error", type(code)) +# #print(code.type) +# # print(read_err) +# return int(code) + + def func_pack_device_init(self): + #切换手动模式 + print("切换手动模式") + self._sys_hand_cmd(True) + time.sleep(1) + while (self._sys_hand_status()) == False: + print("waiting for hand_cmd") + time.sleep(1) + #设备初始化 + self._sys_init_cmd(True) + time.sleep(1) + #sys_init_status为bool值,不加括号 + while (self._sys_init_status())== False: + print("waiting for init_cmd") + time.sleep(1) + #手动按钮置回False + self._sys_hand_cmd(False) + time.sleep(1) + while (self._sys_hand_cmd()) == True: + print("waiting for hand_cmd to False") + time.sleep(1) + #初始化命令置回False + self._sys_init_cmd(False) + time.sleep(1) + while (self._sys_init_cmd()) == True: + print("waiting for init_cmd to False") + time.sleep(1) + + def func_pack_device_auto(self): + #切换自动 + print("切换自动模式") + self._sys_auto_cmd(True) + time.sleep(1) + while (self._sys_auto_status()) == False: + print("waiting for auto_status") + time.sleep(1) + #自动按钮置False + self._sys_auto_cmd(False) + time.sleep(1) + while (self._sys_auto_cmd()) == True: + print("waiting for auto_cmd") + time.sleep(1) + + def func_pack_device_start(self): + #切换自动 + print("启动") + self._sys_start_cmd(True) + time.sleep(1) + while (self._sys_start_status()) == False: + print("waiting for start_status") + time.sleep(1) + #自动按钮置False + self._sys_start_cmd(False) + time.sleep(1) + while (self._sys_start_cmd()) == True: + print("waiting for start_cmd") + time.sleep(1) + + def func_pack_send_bottle_num(self, bottle_num: int): + #发送电解液平台数 + print("启动") + while (self._unilab_rece_electrolyte_bottle_num()) == False: + print("waiting for rece_electrolyte_bottle_num to True") + # self.client.use_node('8520').write(True) + time.sleep(1) + #发送电解液瓶数为2 + self._reg_msg_electrolyte_num(bottle_num) + time.sleep(1) + #完成信号置True + self._unilab_send_electrolyte_bottle_num(True) + time.sleep(1) + #检测到依华已接收 + while (self._unilab_rece_electrolyte_bottle_num()) == True: + print("waiting for rece_electrolyte_bottle_num to False") + time.sleep(1) + #完成信号置False + self._unilab_send_electrolyte_bottle_num(False) + time.sleep(1) + #自动按钮置False + + + # 下发参数 + #def func_pack_send_msg_cmd(self, elec_num: int, elec_use_num: int, elec_vol: float, assembly_type: int, assembly_pressure: int) -> bool: + # """UNILAB写参数""" + # while (self.request_rec_msg_status) == False: + # print("wait for res_msg") + # time.sleep(1) + # self.success = False + # self._unilab_send_msg_electrolyte_num(elec_num) + # time.sleep(1) + # self._unilab_send_msg_electrolyte_use_num(elec_use_num) + # time.sleep(1) + # self._unilab_send_msg_electrolyte_vol(elec_vol) + # time.sleep(1) + # self._unilab_send_msg_assembly_type(assembly_type) + # time.sleep(1) + # self._unilab_send_msg_assembly_pressure(assembly_pressure) + # time.sleep(1) + # self._unilab_send_msg_succ_cmd(True) + # time.sleep(1) + # self._unilab_send_msg_succ_cmd(False) + # #将允许读取标志位置True + # self.allow_data_read = True + # self.success = True + # return self.success + + def func_pack_send_msg_cmd(self, elec_use_num) -> bool: + """UNILAB写参数""" + while (self.request_rec_msg_status) == False: + print("wait for request_rec_msg_status to True") + time.sleep(1) + self.success = False + #self._unilab_send_msg_electrolyte_num(elec_num) + time.sleep(1) + self._unilab_send_msg_electrolyte_use_num(elec_use_num) + time.sleep(1) + self._unilab_send_msg_succ_cmd(True) + time.sleep(1) + while (self.request_rec_msg_status) == True: + print("wait for request_rec_msg_status to False") + time.sleep(1) + self._unilab_send_msg_succ_cmd(False) + #将允许读取标志位置True + self.allow_data_read = True + self.success = True + return self.success + + def func_pack_get_msg_cmd(self, file_path: str="D:\\coin_cell_data") -> bool: + """UNILAB读参数""" + while self.request_send_msg_status == False: + print("waiting for send_read_msg_status to True") + time.sleep(1) + data_open_circuit_voltage = self.data_open_circuit_voltage + data_pole_weight = self.data_pole_weight + data_assembly_time = self.data_assembly_time + data_assembly_pressure = self.data_assembly_pressure + data_electrolyte_volume = self.data_electrolyte_volume + data_coin_num = self.data_coin_num + data_electrolyte_code = self.data_electrolyte_code + data_coin_cell_code = self.data_coin_cell_code + print("data_open_circuit_voltage", data_open_circuit_voltage) + print("data_pole_weight", data_pole_weight) + print("data_assembly_time", data_assembly_time) + print("data_assembly_pressure", data_assembly_pressure) + print("data_electrolyte_volume", data_electrolyte_volume) + print("data_coin_num", data_coin_num) + print("data_electrolyte_code", data_electrolyte_code) + print("data_coin_cell_code", data_coin_cell_code) + #接收完信息后,读取完毕标志位置True + self._unilab_rec_msg_succ_cmd(True) + time.sleep(1) + #等待允许读取标志位置False + while self.request_send_msg_status == True: + print("waiting for send_msg_status to False") + time.sleep(1) + self._unilab_rec_msg_succ_cmd(False) + time.sleep(1) + #将允许读取标志位置True + time_date = datetime.now().strftime("%Y%m%d") + #秒级时间戳用于标记每一行电池数据 + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + #生成输出文件的变量 + self.csv_export_file = os.path.join(file_path, f"date_{time_date}.csv") + #将数据存入csv文件 + if not os.path.exists(self.csv_export_file): + #创建一个表头 + with open(self.csv_export_file, 'w', newline='', encoding='utf-8') as csvfile: + writer = csv.writer(csvfile) + writer.writerow([ + 'Time', 'open_circuit_voltage', 'pole_weight', + 'assembly_time', 'assembly_pressure', 'electrolyte_volume', + 'coin_num', 'electrolyte_code', 'coin_cell_code' + ]) + #立刻写入磁盘 + csvfile.flush() + #开始追加电池信息 + with open(self.csv_export_file, 'a', newline='', encoding='utf-8') as csvfile: + writer = csv.writer(csvfile) + writer.writerow([ + timestamp, data_open_circuit_voltage, data_pole_weight, + data_assembly_time, data_assembly_pressure, data_electrolyte_volume, + data_coin_num, data_electrolyte_code, data_coin_cell_code + ]) + #立刻写入磁盘 + csvfile.flush() + self.success = True + return self.success + + + + def func_pack_send_finished_cmd(self) -> bool: + """UNILAB写参数""" + while (self._unilab_rece_finished_cmd()) == False: + print("wait for rece_finished_cmd to True") + time.sleep(1) + self.success = False + self._unilab_send_finished_cmd(True) + time.sleep(1) + while (self._unilab_rece_finished_cmd()) == True: + print("wait for rece_finished_cmd to False") + time.sleep(1) + self._unilab_send_finished_cmd(False) + #将允许读取标志位置True + self.success = True + return self.success + + + + def func_allpack_cmd(self, elec_num, elec_use_num, file_path: str="D:\\coin_cell_data") -> bool: + summary_csv_file = os.path.join(file_path, "duandian.csv") + # 如果断点文件存在,先读取之前的进度 + if os.path.exists(summary_csv_file): + read_status_flag = True + with open(summary_csv_file, 'r', newline='', encoding='utf-8') as csvfile: + reader = csv.reader(csvfile) + header = next(reader) # 跳过标题行 + data_row = next(reader) # 读取数据行 + if len(data_row) >= 2: + elec_num_r = int(data_row[0]) + elec_use_num_r = int(data_row[1]) + elec_num_N = int(data_row[2]) + elec_use_num_N = int(data_row[3]) + coin_num_N = int(data_row[4]) + if elec_num_r == elec_num and elec_use_num_r == elec_use_num: + print("断点文件与当前任务匹配,继续") + else: + print("断点文件中elec_num、elec_use_num与当前任务不匹配,请检查任务下发参数或修改断点文件") + return False + print(f"从断点文件读取进度: elec_num_N={elec_num_N}, elec_use_num_N={elec_use_num_N}, coin_num_N={coin_num_N}") + + else: + read_status_flag = False + print("未找到断点文件,从头开始") + elec_num_N = 0 + elec_use_num_N = 0 + coin_num_N = 0 + + print(f"剩余电解液瓶数: {elec_num}, 已组装电池数: {elec_use_num}") + + + #如果是第一次运行,则进行初始化、切换自动、启动, 如果是断点重启则跳过。 + if read_status_flag == False: + #初始化 + self.func_pack_device_init() + #切换自动 + self.func_pack_device_auto() + #启动,小车收回 + self.func_pack_device_start() + #发送电解液瓶数量,启动搬运,多搬运没事 + self.func_pack_send_bottle_num(elec_num) + last_i = elec_num_N + last_j = elec_use_num_N + for i in range(last_i, elec_num): + print(f"开始第{last_i+i+1}瓶电解液的组装") + #第一个循环从上次断点继续,后续循环从0开始 + j_start = last_j if i == last_i else 0 + self.func_pack_send_msg_cmd(elec_use_num-j_start) + + for j in range(j_start, elec_use_num): + print(f"开始第{last_i+i+1}瓶电解液的第{j+j_start+1}个电池组装") + #读取电池组装数据并存入csv + self.func_pack_get_msg_cmd(file_path) + time.sleep(1) + + #这里定义物料系统 + # TODO:读完再将电池数加一还是进入循环就将电池数加一需要考虑 + liaopan1 = self.station_resource.get_resource("liaopan1") + liaopan4 = self.station_resource.get_resource("liaopan4") + jipian1 = liaopan1.children[coin_num_N].children[0] + jipian4 = liaopan4.children[coin_num_N].children[0] + #print(jipian1) + #从料盘上去物料解绑后放到另一盘上 + jipian1.parent.unassign_child_resource(jipian1) + jipian4.parent.unassign_child_resource(jipian4) + + #print(jipian2.parent) + battery = Battery(name = f"battery_{coin_num_N}") + battery.assign_child_resource(jipian1, location=None) + battery.assign_child_resource(jipian4, location=None) + + zidanjia6 = self.station_resource.get_resource("zi_dan_jia6") + + zidanjia6.children[0].assign_child_resource(battery, location=None) + + + # 生成断点文件 + # 生成包含elec_num_N、coin_num_N、timestamp的CSV文件 + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + with open(summary_csv_file, 'w', newline='', encoding='utf-8') as csvfile: + writer = csv.writer(csvfile) + writer.writerow(['elec_num','elec_use_num', 'elec_num_N', 'elec_use_num_N', 'coin_num_N', 'timestamp']) + writer.writerow([elec_num, elec_use_num, elec_num_N, elec_use_num_N, coin_num_N, timestamp]) + csvfile.flush() + coin_num_N += 1 + elec_use_num_N += 1 + elec_num_N += 1 + elec_use_num_N = 0 + + #循环正常结束,则删除断点文件 + os.remove(summary_csv_file) + #全部完成后等待依华发送完成信号 + self.func_pack_send_finished_cmd() + + + def func_pack_device_stop(self) -> bool: + """打包指令:设备停止""" + for i in range(3): + time.sleep(2) + print(f"输出{i}") + #print("_sys_hand_cmd", self._sys_hand_cmd()) + #time.sleep(1) + #print("_sys_hand_status", self._sys_hand_status()) + #time.sleep(1) + #print("_sys_init_cmd", self._sys_init_cmd()) + #time.sleep(1) + #print("_sys_init_status", self._sys_init_status()) + #time.sleep(1) + #print("_sys_auto_status", self._sys_auto_status()) + #time.sleep(1) + #print("data_axis_y_pos", self.data_axis_y_pos) + #time.sleep(1) + #self.success = False + #with open('action_device_stop.json', 'r', encoding='utf-8') as f: + # action_json = json.load(f) + #self.client.execute_procedure_from_json(action_json) + #self.success = True + #return self.success + + def fun_wuliao_test(self) -> bool: + #找到data_init中构建的2个物料盘 + #liaopan1 = self.station_resource.get_resource("liaopan1") + #liaopan4 = self.station_resource.get_resource("liaopan4") + #for coin_num_N in range(16): + # liaopan1 = self.station_resource.get_resource("liaopan1") + # liaopan4 = self.station_resource.get_resource("liaopan4") + # jipian1 = liaopan1.children[coin_num_N].children[0] + # jipian4 = liaopan4.children[coin_num_N].children[0] + # #print(jipian1) + # #从料盘上去物料解绑后放到另一盘上 + # jipian1.parent.unassign_child_resource(jipian1) + # jipian4.parent.unassign_child_resource(jipian4) + # + # #print(jipian2.parent) + # battery = Battery(name = f"battery_{coin_num_N}") + # battery.assign_child_resource(jipian1, location=None) + # battery.assign_child_resource(jipian4, location=None) + # + # zidanjia6 = self.station_resource.get_resource("zi_dan_jia6") + # zidanjia6.children[0].assign_child_resource(battery, location=None) + # ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ + # "resources": [self.station_resource] + # }) + # time.sleep(2) + for i in range(20): + print(f"输出{i}") + time.sleep(2) + + + # 数据读取与输出 + def func_read_data_and_output(self, file_path: str="D:\\coin_cell_data"): + # 检查CSV导出是否正在运行,已运行则跳出,防止同时启动两个while循环 + if self.csv_export_running: + return False, "读取已在运行中" + + #若不存在该目录则创建 + if not os.path.exists(file_path): + os.makedirs(file_path) + print(f"创建目录: {file_path}") + + # 只要允许读取标志位为true,就持续运行该函数,直到触发停止条件 + while self.allow_data_read: + + #函数运行标志位,确保只同时启动一个导出函数 + self.csv_export_running = True + + #等待接收结果标志位置True + while self.request_send_msg_status == False: + print("waiting for send_msg_status to True") + time.sleep(1) + #日期时间戳用于按天存放csv文件 + time_date = datetime.now().strftime("%Y%m%d") + #秒级时间戳用于标记每一行电池数据 + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + #生成输出文件的变量 + self.csv_export_file = os.path.join(file_path, f"date_{time_date}.csv") + + #接收信息 + data_open_circuit_voltage = self.data_open_circuit_voltage + data_pole_weight = self.data_pole_weight + data_assembly_time = self.data_assembly_time + data_assembly_pressure = self.data_assembly_pressure + data_electrolyte_volume = self.data_electrolyte_volume + data_coin_num = self.data_coin_num + data_electrolyte_code = self.data_electrolyte_code + data_coin_cell_code = self.data_coin_cell_code + # 电解液瓶位置 + elec_bottle_site = 2 + # 极片夹取位置(应当通过寄存器读光标) + Pos_elec_site = 0 + Al_elec_site = 0 + Gasket_site = 0 + + #接收完信息后,读取完毕标志位置True + self._unilab_rec_msg_succ_cmd()# = True + #等待允许读取标志位置False + while self.request_send_msg_status == True: + print("waiting for send_msg_status to False") + time.sleep(1) + self._unilab_rec_msg_succ_cmd()# = False + + #此处操作物料信息(如果中途报错停止,如何) + #报错怎么办(加个判断标志位,如果发生错误,则根据停止位置扣除物料) + #根据物料光标判断取哪个物料(人工摆盘,电解液瓶,移液枪头都有光标位置,寄存器读即可) + + #物料读取操作写在这里 + #在这里进行物料调取 + #转移物料瓶,elec_bottle_site对应第几瓶电解液(从依华寄存器读取) + # transfer_bottle(deck, elec_bottle_site) + # #找到电解液瓶的对象 + # electrolyte_rack = deck.get_resource("electrolyte_rack") + # pending_positions = electrolyte_rack.get_pending_positions()[elec_bottle_site] + # # TODO: 瓶子取液体操作需要加入 +# +# + # #找到压制工站对应的对象 + # battery_press_slot = deck.get_resource("battery_press_1") + # #创建一个新电池 + # test_battery = Battery( + # name=f"test_battery_{data_coin_num}", + # diameter=20.0, # 与压制槽直径匹配 + # height=3.0, # 电池高度 + # max_volume=100.0, # 100μL容量 + # barcode=data_coin_cell_code, # 电池条码 + # ) + # if battery_press_slot.has_battery(): + # return False, "压制工站已有电池,无法放置新电池" + # #在压制位放置电池 + # battery_press_slot.place_battery(test_battery) + # #从第一个子弹夹中取料 + # clip_magazine_1_hole = self.deck.get_resource("clip_magazine_1").get_item(Pos_elec_site) + # clip_magazine_2_hole = self.deck.get_resource("clip_magazine_2").get_item(Al_elec_site) + # clip_magazine_3_hole = self.deck.get_resource("clip_magazine_3").get_item(Gasket_site) + # + # if clip_magazine_1_hole.get_sheet_count() > 0: # 检查洞位是否有极片 + # electrode_sheet_1 = clip_magazine_1_hole.take_sheet() # 从洞位取出极片 + # test_battery.add_electrode_sheet(electrode_sheet_1) # 添加到电池中 + # print(f"已将极片 {electrode_sheet_1.name} 从子弹夹转移到电池") + # else: + # print("子弹夹洞位0没有极片") +# + # if clip_magazine_2_hole.get_sheet_count() > 0: # 检查洞位是否有极片 + # electrode_sheet_2 = clip_magazine_2_hole.take_sheet() # 从洞位取出极片 + # test_battery.add_electrode_sheet(electrode_sheet_2) # 添加到电池中 + # print(f"已将极片 {electrode_sheet_2.name} 从子弹夹转移到电池") + # else: + # print("子弹夹洞位0没有极片") +# + # if clip_magazine_3_hole.get_sheet_count() > 0: # 检查洞位是否有极片 + # electrode_sheet_3 = clip_magazine_3_hole.take_sheet() # 从洞位取出极片 + # test_battery.add_electrode_sheet(electrode_sheet_3) # 添加到电池中 + # print(f"已将极片 {electrode_sheet_3.name} 从子弹夹转移到电池") + # else: + # print("子弹夹洞位0没有极片") + # + # # TODO:#把电解液从瓶中取到电池夹子中 + # battery_site = deck.get_resource("battery_press_1") + # clip_magazine_battery = deck.get_resource("clip_magazine_battery") + # if battery_site.has_battery(): + # battery = battery_site.take_battery() #从压制槽取出电池 + # clip_magazine_battery.add_battery(battery) #从压制槽取出电池 +# +# +# +# + # # 保存配置到文件 + # self.deck.save("button_battery_station_layout.json", indent=2) + # print("\n台面配置已保存到: button_battery_station_layout.json") + # + # # 保存状态到文件 + # self.deck.save_state_to_file("button_battery_station_state.json", indent=2) + # print("台面状态已保存到: button_battery_station_state.json") + + + + + + + #将数据写入csv中 + #如当前目录下无同名文件则新建一个csv用于存放数据 + if not os.path.exists(self.csv_export_file): + #创建一个表头 + with open(self.csv_export_file, 'w', newline='', encoding='utf-8') as csvfile: + writer = csv.writer(csvfile) + writer.writerow([ + 'Time', 'open_circuit_voltage', 'pole_weight', + 'assembly_time', 'assembly_pressure', 'electrolyte_volume', + 'coin_num', 'electrolyte_code', 'coin_cell_code' + ]) + #立刻写入磁盘 + csvfile.flush() + #开始追加电池信息 + with open(self.csv_export_file, 'a', newline='', encoding='utf-8') as csvfile: + writer = csv.writer(csvfile) + writer.writerow([ + timestamp, data_open_circuit_voltage, data_pole_weight, + data_assembly_time, data_assembly_pressure, data_electrolyte_volume, + data_coin_num, data_electrolyte_code, data_coin_cell_code + ]) + #立刻写入磁盘 + csvfile.flush() + + # 只要不在自动模式运行中,就将允许标志位置False + if self.sys_auto_status == False or self.sys_start_status == False: + self.allow_data_read = False + self.csv_export_running = False + time.sleep(1) + + def func_stop_read_data(self): + """停止CSV导出""" + if not self.csv_export_running: + return False, "read data未在运行" + + self.csv_export_running = False + self.allow_data_read = False + + if self.csv_export_thread and self.csv_export_thread.is_alive(): + self.csv_export_thread.join(timeout=5) + + def func_get_csv_export_status(self): + """获取CSV导出状态""" + return { + 'allow_read': self.allow_data_read, + 'running': self.csv_export_running, + 'thread_alive': self.csv_export_thread.is_alive() if self.csv_export_thread else False + } + + + ''' + # ===================== 物料管理区 ====================== + @property + def data_material_inventory(self) -> int: + """主物料库存 (数量, INT16)""" + inventory, read_err = self.client.use_node('REG_DATA_MATERIAL_INVENTORY').read(1) + return inventory + + @property + def data_tips_inventory(self) -> int: + """移液枪头库存 (数量, INT16)""" + inventory, read_err = self.client.register_node_list(self.nodes).use_node('REG_DATA_TIPS_INVENTORY').read(1) + return inventory + + ''' + + +if __name__ == "__main__": + from pylabrobot.resources import Resource + Coin_Cell = CoinCellAssemblyWorkstation(Resource("1", 1, 1, 1), debug_mode=True) + #Coin_Cell.func_pack_device_init() + #Coin_Cell.func_pack_device_auto() + #Coin_Cell.func_pack_device_start() + #Coin_Cell.func_pack_send_bottle_num(2) + #Coin_Cell.func_pack_send_msg_cmd(2) + #Coin_Cell.func_pack_get_msg_cmd() + #Coin_Cell.func_pack_get_msg_cmd() + #Coin_Cell.func_pack_send_finished_cmd() +# + #Coin_Cell.func_allpack_cmd(3, 2) + #print(Coin_Cell.data_stack_vision_code) + #print("success") + #创建一个物料台面 + + #deck = create_a_coin_cell_deck() + + ##在台面上找到料盘和极片 + #liaopan1 = deck.get_resource("liaopan1") + #liaopan2 = deck.get_resource("liaopan2") + #jipian1 = liaopan1.children[1].children[0] +# + ##print(jipian1) + ##把物料解绑后放到另一盘上 + #jipian1.parent.unassign_child_resource(jipian1) + #liaopan2.children[1].assign_child_resource(jipian1, location=None) + ##print(jipian2.parent) + from unilabos.resources.graphio import resource_ulab_to_plr, convert_resources_to_type + + with open("./button_battery_station_resources_unilab.json", "r", encoding="utf-8") as f: + bioyond_resources_unilab = json.load(f) + print(f"成功读取 JSON 文件,包含 {len(bioyond_resources_unilab)} 个资源") + ulab_resources = convert_resources_to_type(bioyond_resources_unilab, List[PLRResource]) + print(f"转换结果类型: {type(ulab_resources)}") + print(ulab_resources) + diff --git a/unilabos/devices/workstation/coin_cell_assembly/new_cellconfig4.json b/unilabos/devices/workstation/coin_cell_assembly/new_cellconfig4.json new file mode 100644 index 00000000..0ba79b72 --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/new_cellconfig4.json @@ -0,0 +1,14472 @@ +{ + "nodes": [ + { + "id": "BatteryStation", + "name": "扣电工作站", + "children": [ + "coin_cell_deck" + ], + "parent": null, + "type": "device", + "class": "bettery_station_registry", + "position": { + "x": 600, + "y": 400, + "z": 0 + }, + "config": { + "debug_mode": false, + "_comment": "protocol_type接外部工站固定写法字段,一般为空,station_resource写法也固定", + "protocol_type": [], + "station_resource": { + "data": { + "_resource_child_name": "coin_cell_deck", + "_resource_type": "unilabos.devices.workstation.coin_cell_assembly.button_battery_station:CoincellDeck" + } + }, + + "address": "192.168.1.20", + "port": 502 + }, + "data": {} + }, + { + "id": "coin_cell_deck", + "name": "coin_cell_deck", + "sample_id": null, + "children": [ + "zi_dan_jia", + "zi_dan_jia2", + "zi_dan_jia3", + "zi_dan_jia4", + "zi_dan_jia5", + "zi_dan_jia6", + "zi_dan_jia7", + "zi_dan_jia8", + "liaopan1", + "liaopan2", + "liaopan3", + "liaopan4", + "liaopan5", + "liaopan6", + "bottle_rack_3x4", + "bottle_rack_6x2", + "bottle_rack_6x2_2", + "tip_box_64", + "waste_tip_box" + ], + "parent": null, + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "CoincellDeck", + "size_x": 1620.0, + "size_y": 1270.0, + "size_z": 500.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "coin_cell_deck", + "barcode": null + }, + "data": {} + }, + { + "id": "zi_dan_jia", + "name": "zi_dan_jia", + "sample_id": null, + "children": [ + "zi_dan_jia_clipmagazinehole_0_0", + "zi_dan_jia_clipmagazinehole_0_1", + "zi_dan_jia_clipmagazinehole_1_0", + "zi_dan_jia_clipmagazinehole_1_1" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1400, + "y": 50, + "z": 0 + }, + "config": { + "type": "ClipMagazine_four", + "size_x": 80, + "size_y": 80, + "size_z": 10, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_four", + "model": null, + "barcode": null, + "ordering": { + "A1": "zi_dan_jia_clipmagazinehole_0_0", + "B1": "zi_dan_jia_clipmagazinehole_0_1", + "A2": "zi_dan_jia_clipmagazinehole_1_0", + "B2": "zi_dan_jia_clipmagazinehole_1_1" + }, + "hole_diameter": 14.0, + "hole_depth": 10.0, + "max_sheets_per_hole": 100 + }, + "data": {} + }, + { + "id": "zi_dan_jia_clipmagazinehole_0_0", + "name": "zi_dan_jia_clipmagazinehole_0_0", + "sample_id": null, + "children": [ + "zi_dan_jia2_jipian_0" + ], + "parent": "zi_dan_jia", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia2_jipian_0", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia_clipmagazinehole_0_0" + } + ] + } + }, + { + "id": "zi_dan_jia2_jipian_0", + "name": "zi_dan_jia2_jipian_0", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia_clipmagazinehole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia_clipmagazinehole_0_1", + "name": "zi_dan_jia_clipmagazinehole_0_1", + "sample_id": null, + "children": [ + "zi_dan_jia2_jipian_1" + ], + "parent": "zi_dan_jia", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia2_jipian_1", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia_clipmagazinehole_0_1" + } + ] + } + }, + { + "id": "zi_dan_jia2_jipian_1", + "name": "zi_dan_jia2_jipian_1", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia_clipmagazinehole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia_clipmagazinehole_1_0", + "name": "zi_dan_jia_clipmagazinehole_1_0", + "sample_id": null, + "children": [ + "zi_dan_jia2_jipian_2" + ], + "parent": "zi_dan_jia", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia2_jipian_2", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia_clipmagazinehole_1_0" + } + ] + } + }, + { + "id": "zi_dan_jia2_jipian_2", + "name": "zi_dan_jia2_jipian_2", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia_clipmagazinehole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia_clipmagazinehole_1_1", + "name": "zi_dan_jia_clipmagazinehole_1_1", + "sample_id": null, + "children": [ + "zi_dan_jia2_jipian_3" + ], + "parent": "zi_dan_jia", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia2_jipian_3", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia_clipmagazinehole_1_1" + } + ] + } + }, + { + "id": "zi_dan_jia2_jipian_3", + "name": "zi_dan_jia2_jipian_3", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia_clipmagazinehole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia2", + "name": "zi_dan_jia2", + "sample_id": null, + "children": [ + "zi_dan_jia2_clipmagazinehole_0_0", + "zi_dan_jia2_clipmagazinehole_0_1", + "zi_dan_jia2_clipmagazinehole_1_0", + "zi_dan_jia2_clipmagazinehole_1_1" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1600, + "y": 200, + "z": 0 + }, + "config": { + "type": "ClipMagazine_four", + "size_x": 80, + "size_y": 80, + "size_z": 10, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_four", + "model": null, + "barcode": null, + "ordering": { + "A1": "zi_dan_jia2_clipmagazinehole_0_0", + "B1": "zi_dan_jia2_clipmagazinehole_0_1", + "A2": "zi_dan_jia2_clipmagazinehole_1_0", + "B2": "zi_dan_jia2_clipmagazinehole_1_1" + }, + "hole_diameter": 14.0, + "hole_depth": 10.0, + "max_sheets_per_hole": 100 + }, + "data": {} + }, + { + "id": "zi_dan_jia2_clipmagazinehole_0_0", + "name": "zi_dan_jia2_clipmagazinehole_0_0", + "sample_id": null, + "children": [ + "zi_dan_jia_jipian_0" + ], + "parent": "zi_dan_jia2", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia_jipian_0", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia2_clipmagazinehole_0_0" + } + ] + } + }, + { + "id": "zi_dan_jia_jipian_0", + "name": "zi_dan_jia_jipian_0", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia2_clipmagazinehole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia2_clipmagazinehole_0_1", + "name": "zi_dan_jia2_clipmagazinehole_0_1", + "sample_id": null, + "children": [ + "zi_dan_jia_jipian_1" + ], + "parent": "zi_dan_jia2", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia_jipian_1", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia2_clipmagazinehole_0_1" + } + ] + } + }, + { + "id": "zi_dan_jia_jipian_1", + "name": "zi_dan_jia_jipian_1", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia2_clipmagazinehole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia2_clipmagazinehole_1_0", + "name": "zi_dan_jia2_clipmagazinehole_1_0", + "sample_id": null, + "children": [ + "zi_dan_jia_jipian_2" + ], + "parent": "zi_dan_jia2", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia_jipian_2", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia2_clipmagazinehole_1_0" + } + ] + } + }, + { + "id": "zi_dan_jia_jipian_2", + "name": "zi_dan_jia_jipian_2", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia2_clipmagazinehole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia2_clipmagazinehole_1_1", + "name": "zi_dan_jia2_clipmagazinehole_1_1", + "sample_id": null, + "children": [ + "zi_dan_jia_jipian_3" + ], + "parent": "zi_dan_jia2", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia_jipian_3", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia2_clipmagazinehole_1_1" + } + ] + } + }, + { + "id": "zi_dan_jia_jipian_3", + "name": "zi_dan_jia_jipian_3", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia2_clipmagazinehole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia3", + "name": "zi_dan_jia3", + "sample_id": null, + "children": [ + "zi_dan_jia3_clipmagazinehole_0_0", + "zi_dan_jia3_clipmagazinehole_0_1", + "zi_dan_jia3_clipmagazinehole_1_0", + "zi_dan_jia3_clipmagazinehole_1_1", + "zi_dan_jia3_clipmagazinehole_2_0", + "zi_dan_jia3_clipmagazinehole_2_1" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1500, + "y": 200, + "z": 0 + }, + "config": { + "type": "ClipMagazine", + "size_x": 80, + "size_y": 80, + "size_z": 10, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine", + "model": null, + "barcode": null, + "ordering": { + "A1": "zi_dan_jia3_clipmagazinehole_0_0", + "B1": "zi_dan_jia3_clipmagazinehole_0_1", + "A2": "zi_dan_jia3_clipmagazinehole_1_0", + "B2": "zi_dan_jia3_clipmagazinehole_1_1", + "A3": "zi_dan_jia3_clipmagazinehole_2_0", + "B3": "zi_dan_jia3_clipmagazinehole_2_1" + }, + "hole_diameter": 14.0, + "hole_depth": 10.0, + "max_sheets_per_hole": 100 + }, + "data": {} + }, + { + "id": "zi_dan_jia3_clipmagazinehole_0_0", + "name": "zi_dan_jia3_clipmagazinehole_0_0", + "sample_id": null, + "children": [ + "zi_dan_jia3_jipian_0" + ], + "parent": "zi_dan_jia3", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia3_jipian_0", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia3_clipmagazinehole_0_0" + } + ] + } + }, + { + "id": "zi_dan_jia3_jipian_0", + "name": "zi_dan_jia3_jipian_0", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia3_clipmagazinehole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia3_clipmagazinehole_0_1", + "name": "zi_dan_jia3_clipmagazinehole_0_1", + "sample_id": null, + "children": [ + "zi_dan_jia3_jipian_1" + ], + "parent": "zi_dan_jia3", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia3_jipian_1", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia3_clipmagazinehole_0_1" + } + ] + } + }, + { + "id": "zi_dan_jia3_jipian_1", + "name": "zi_dan_jia3_jipian_1", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia3_clipmagazinehole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia3_clipmagazinehole_1_0", + "name": "zi_dan_jia3_clipmagazinehole_1_0", + "sample_id": null, + "children": [ + "zi_dan_jia3_jipian_2" + ], + "parent": "zi_dan_jia3", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia3_jipian_2", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia3_clipmagazinehole_1_0" + } + ] + } + }, + { + "id": "zi_dan_jia3_jipian_2", + "name": "zi_dan_jia3_jipian_2", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia3_clipmagazinehole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia3_clipmagazinehole_1_1", + "name": "zi_dan_jia3_clipmagazinehole_1_1", + "sample_id": null, + "children": [ + "zi_dan_jia3_jipian_3" + ], + "parent": "zi_dan_jia3", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia3_jipian_3", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia3_clipmagazinehole_1_1" + } + ] + } + }, + { + "id": "zi_dan_jia3_jipian_3", + "name": "zi_dan_jia3_jipian_3", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia3_clipmagazinehole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia3_clipmagazinehole_2_0", + "name": "zi_dan_jia3_clipmagazinehole_2_0", + "sample_id": null, + "children": [ + "zi_dan_jia3_jipian_4" + ], + "parent": "zi_dan_jia3", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia3_jipian_4", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia3_clipmagazinehole_2_0" + } + ] + } + }, + { + "id": "zi_dan_jia3_jipian_4", + "name": "zi_dan_jia3_jipian_4", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia3_clipmagazinehole_2_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia3_clipmagazinehole_2_1", + "name": "zi_dan_jia3_clipmagazinehole_2_1", + "sample_id": null, + "children": [ + "zi_dan_jia3_jipian_5" + ], + "parent": "zi_dan_jia3", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia3_jipian_5", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia3_clipmagazinehole_2_1" + } + ] + } + }, + { + "id": "zi_dan_jia3_jipian_5", + "name": "zi_dan_jia3_jipian_5", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia3_clipmagazinehole_2_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia4", + "name": "zi_dan_jia4", + "sample_id": null, + "children": [ + "zi_dan_jia4_clipmagazinehole_0_0", + "zi_dan_jia4_clipmagazinehole_0_1", + "zi_dan_jia4_clipmagazinehole_1_0", + "zi_dan_jia4_clipmagazinehole_1_1", + "zi_dan_jia4_clipmagazinehole_2_0", + "zi_dan_jia4_clipmagazinehole_2_1" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1500, + "y": 300, + "z": 0 + }, + "config": { + "type": "ClipMagazine", + "size_x": 80, + "size_y": 80, + "size_z": 10, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine", + "model": null, + "barcode": null, + "ordering": { + "A1": "zi_dan_jia4_clipmagazinehole_0_0", + "B1": "zi_dan_jia4_clipmagazinehole_0_1", + "A2": "zi_dan_jia4_clipmagazinehole_1_0", + "B2": "zi_dan_jia4_clipmagazinehole_1_1", + "A3": "zi_dan_jia4_clipmagazinehole_2_0", + "B3": "zi_dan_jia4_clipmagazinehole_2_1" + }, + "hole_diameter": 14.0, + "hole_depth": 10.0, + "max_sheets_per_hole": 100 + }, + "data": {} + }, + { + "id": "zi_dan_jia4_clipmagazinehole_0_0", + "name": "zi_dan_jia4_clipmagazinehole_0_0", + "sample_id": null, + "children": [ + "zi_dan_jia4_jipian_0" + ], + "parent": "zi_dan_jia4", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia4_jipian_0", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia4_clipmagazinehole_0_0" + } + ] + } + }, + { + "id": "zi_dan_jia4_jipian_0", + "name": "zi_dan_jia4_jipian_0", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia4_clipmagazinehole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia4_clipmagazinehole_0_1", + "name": "zi_dan_jia4_clipmagazinehole_0_1", + "sample_id": null, + "children": [ + "zi_dan_jia4_jipian_1" + ], + "parent": "zi_dan_jia4", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia4_jipian_1", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia4_clipmagazinehole_0_1" + } + ] + } + }, + { + "id": "zi_dan_jia4_jipian_1", + "name": "zi_dan_jia4_jipian_1", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia4_clipmagazinehole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia4_clipmagazinehole_1_0", + "name": "zi_dan_jia4_clipmagazinehole_1_0", + "sample_id": null, + "children": [ + "zi_dan_jia4_jipian_2" + ], + "parent": "zi_dan_jia4", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia4_jipian_2", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia4_clipmagazinehole_1_0" + } + ] + } + }, + { + "id": "zi_dan_jia4_jipian_2", + "name": "zi_dan_jia4_jipian_2", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia4_clipmagazinehole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia4_clipmagazinehole_1_1", + "name": "zi_dan_jia4_clipmagazinehole_1_1", + "sample_id": null, + "children": [ + "zi_dan_jia4_jipian_3" + ], + "parent": "zi_dan_jia4", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia4_jipian_3", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia4_clipmagazinehole_1_1" + } + ] + } + }, + { + "id": "zi_dan_jia4_jipian_3", + "name": "zi_dan_jia4_jipian_3", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia4_clipmagazinehole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia4_clipmagazinehole_2_0", + "name": "zi_dan_jia4_clipmagazinehole_2_0", + "sample_id": null, + "children": [ + "zi_dan_jia4_jipian_4" + ], + "parent": "zi_dan_jia4", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia4_jipian_4", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia4_clipmagazinehole_2_0" + } + ] + } + }, + { + "id": "zi_dan_jia4_jipian_4", + "name": "zi_dan_jia4_jipian_4", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia4_clipmagazinehole_2_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia4_clipmagazinehole_2_1", + "name": "zi_dan_jia4_clipmagazinehole_2_1", + "sample_id": null, + "children": [ + "zi_dan_jia4_jipian_5" + ], + "parent": "zi_dan_jia4", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia4_jipian_5", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia4_clipmagazinehole_2_1" + } + ] + } + }, + { + "id": "zi_dan_jia4_jipian_5", + "name": "zi_dan_jia4_jipian_5", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia4_clipmagazinehole_2_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia5", + "name": "zi_dan_jia5", + "sample_id": null, + "children": [ + "zi_dan_jia5_clipmagazinehole_0_0", + "zi_dan_jia5_clipmagazinehole_0_1", + "zi_dan_jia5_clipmagazinehole_1_0", + "zi_dan_jia5_clipmagazinehole_1_1", + "zi_dan_jia5_clipmagazinehole_2_0", + "zi_dan_jia5_clipmagazinehole_2_1" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1600, + "y": 300, + "z": 0 + }, + "config": { + "type": "ClipMagazine", + "size_x": 80, + "size_y": 80, + "size_z": 10, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine", + "model": null, + "barcode": null, + "ordering": { + "A1": "zi_dan_jia5_clipmagazinehole_0_0", + "B1": "zi_dan_jia5_clipmagazinehole_0_1", + "A2": "zi_dan_jia5_clipmagazinehole_1_0", + "B2": "zi_dan_jia5_clipmagazinehole_1_1", + "A3": "zi_dan_jia5_clipmagazinehole_2_0", + "B3": "zi_dan_jia5_clipmagazinehole_2_1" + }, + "hole_diameter": 14.0, + "hole_depth": 10.0, + "max_sheets_per_hole": 100 + }, + "data": {} + }, + { + "id": "zi_dan_jia5_clipmagazinehole_0_0", + "name": "zi_dan_jia5_clipmagazinehole_0_0", + "sample_id": null, + "children": [ + "zi_dan_jia5_jipian_0" + ], + "parent": "zi_dan_jia5", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia5_jipian_0", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia5_clipmagazinehole_0_0" + } + ] + } + }, + { + "id": "zi_dan_jia5_jipian_0", + "name": "zi_dan_jia5_jipian_0", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia5_clipmagazinehole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia5_clipmagazinehole_0_1", + "name": "zi_dan_jia5_clipmagazinehole_0_1", + "sample_id": null, + "children": [ + "zi_dan_jia5_jipian_1" + ], + "parent": "zi_dan_jia5", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia5_jipian_1", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia5_clipmagazinehole_0_1" + } + ] + } + }, + { + "id": "zi_dan_jia5_jipian_1", + "name": "zi_dan_jia5_jipian_1", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia5_clipmagazinehole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia5_clipmagazinehole_1_0", + "name": "zi_dan_jia5_clipmagazinehole_1_0", + "sample_id": null, + "children": [ + "zi_dan_jia5_jipian_2" + ], + "parent": "zi_dan_jia5", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia5_jipian_2", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia5_clipmagazinehole_1_0" + } + ] + } + }, + { + "id": "zi_dan_jia5_jipian_2", + "name": "zi_dan_jia5_jipian_2", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia5_clipmagazinehole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia5_clipmagazinehole_1_1", + "name": "zi_dan_jia5_clipmagazinehole_1_1", + "sample_id": null, + "children": [ + "zi_dan_jia5_jipian_3" + ], + "parent": "zi_dan_jia5", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia5_jipian_3", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia5_clipmagazinehole_1_1" + } + ] + } + }, + { + "id": "zi_dan_jia5_jipian_3", + "name": "zi_dan_jia5_jipian_3", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia5_clipmagazinehole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia5_clipmagazinehole_2_0", + "name": "zi_dan_jia5_clipmagazinehole_2_0", + "sample_id": null, + "children": [ + "zi_dan_jia5_jipian_4" + ], + "parent": "zi_dan_jia5", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia5_jipian_4", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia5_clipmagazinehole_2_0" + } + ] + } + }, + { + "id": "zi_dan_jia5_jipian_4", + "name": "zi_dan_jia5_jipian_4", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia5_clipmagazinehole_2_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia5_clipmagazinehole_2_1", + "name": "zi_dan_jia5_clipmagazinehole_2_1", + "sample_id": null, + "children": [ + "zi_dan_jia5_jipian_5" + ], + "parent": "zi_dan_jia5", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia5_jipian_5", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia5_clipmagazinehole_2_1" + } + ] + } + }, + { + "id": "zi_dan_jia5_jipian_5", + "name": "zi_dan_jia5_jipian_5", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia5_clipmagazinehole_2_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia6", + "name": "zi_dan_jia6", + "sample_id": null, + "children": [ + "zi_dan_jia6_clipmagazinehole_0_0", + "zi_dan_jia6_clipmagazinehole_0_1", + "zi_dan_jia6_clipmagazinehole_1_0", + "zi_dan_jia6_clipmagazinehole_1_1", + "zi_dan_jia6_clipmagazinehole_2_0", + "zi_dan_jia6_clipmagazinehole_2_1" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1530, + "y": 500, + "z": 0 + }, + "config": { + "type": "ClipMagazine", + "size_x": 80, + "size_y": 80, + "size_z": 10, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine", + "model": null, + "barcode": null, + "ordering": { + "A1": "zi_dan_jia6_clipmagazinehole_0_0", + "B1": "zi_dan_jia6_clipmagazinehole_0_1", + "A2": "zi_dan_jia6_clipmagazinehole_1_0", + "B2": "zi_dan_jia6_clipmagazinehole_1_1", + "A3": "zi_dan_jia6_clipmagazinehole_2_0", + "B3": "zi_dan_jia6_clipmagazinehole_2_1" + }, + "hole_diameter": 14.0, + "hole_depth": 10.0, + "max_sheets_per_hole": 100 + }, + "data": {} + }, + { + "id": "zi_dan_jia6_clipmagazinehole_0_0", + "name": "zi_dan_jia6_clipmagazinehole_0_0", + "sample_id": null, + "children": [ + "zi_dan_jia6_jipian_0" + ], + "parent": "zi_dan_jia6", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia6_jipian_0", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia6_clipmagazinehole_0_0" + } + ] + } + }, + { + "id": "zi_dan_jia6_jipian_0", + "name": "zi_dan_jia6_jipian_0", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia6_clipmagazinehole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia6_clipmagazinehole_0_1", + "name": "zi_dan_jia6_clipmagazinehole_0_1", + "sample_id": null, + "children": [ + "zi_dan_jia6_jipian_1" + ], + "parent": "zi_dan_jia6", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia6_jipian_1", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia6_clipmagazinehole_0_1" + } + ] + } + }, + { + "id": "zi_dan_jia6_jipian_1", + "name": "zi_dan_jia6_jipian_1", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia6_clipmagazinehole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia6_clipmagazinehole_1_0", + "name": "zi_dan_jia6_clipmagazinehole_1_0", + "sample_id": null, + "children": [ + "zi_dan_jia6_jipian_2" + ], + "parent": "zi_dan_jia6", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia6_jipian_2", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia6_clipmagazinehole_1_0" + } + ] + } + }, + { + "id": "zi_dan_jia6_jipian_2", + "name": "zi_dan_jia6_jipian_2", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia6_clipmagazinehole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia6_clipmagazinehole_1_1", + "name": "zi_dan_jia6_clipmagazinehole_1_1", + "sample_id": null, + "children": [ + "zi_dan_jia6_jipian_3" + ], + "parent": "zi_dan_jia6", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia6_jipian_3", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia6_clipmagazinehole_1_1" + } + ] + } + }, + { + "id": "zi_dan_jia6_jipian_3", + "name": "zi_dan_jia6_jipian_3", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia6_clipmagazinehole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia6_clipmagazinehole_2_0", + "name": "zi_dan_jia6_clipmagazinehole_2_0", + "sample_id": null, + "children": [ + "zi_dan_jia6_jipian_4" + ], + "parent": "zi_dan_jia6", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia6_jipian_4", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia6_clipmagazinehole_2_0" + } + ] + } + }, + { + "id": "zi_dan_jia6_jipian_4", + "name": "zi_dan_jia6_jipian_4", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia6_clipmagazinehole_2_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia6_clipmagazinehole_2_1", + "name": "zi_dan_jia6_clipmagazinehole_2_1", + "sample_id": null, + "children": [ + "zi_dan_jia6_jipian_5" + ], + "parent": "zi_dan_jia6", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia6_jipian_5", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia6_clipmagazinehole_2_1" + } + ] + } + }, + { + "id": "zi_dan_jia6_jipian_5", + "name": "zi_dan_jia6_jipian_5", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia6_clipmagazinehole_2_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia7", + "name": "zi_dan_jia7", + "sample_id": null, + "children": [ + "zi_dan_jia7_clipmagazinehole_0_0", + "zi_dan_jia7_clipmagazinehole_0_1", + "zi_dan_jia7_clipmagazinehole_1_0", + "zi_dan_jia7_clipmagazinehole_1_1", + "zi_dan_jia7_clipmagazinehole_2_0", + "zi_dan_jia7_clipmagazinehole_2_1" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1180, + "y": 400, + "z": 0 + }, + "config": { + "type": "ClipMagazine", + "size_x": 80, + "size_y": 80, + "size_z": 10, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine", + "model": null, + "barcode": null, + "ordering": { + "A1": "zi_dan_jia7_clipmagazinehole_0_0", + "B1": "zi_dan_jia7_clipmagazinehole_0_1", + "A2": "zi_dan_jia7_clipmagazinehole_1_0", + "B2": "zi_dan_jia7_clipmagazinehole_1_1", + "A3": "zi_dan_jia7_clipmagazinehole_2_0", + "B3": "zi_dan_jia7_clipmagazinehole_2_1" + }, + "hole_diameter": 14.0, + "hole_depth": 10.0, + "max_sheets_per_hole": 100 + }, + "data": {} + }, + { + "id": "zi_dan_jia7_clipmagazinehole_0_0", + "name": "zi_dan_jia7_clipmagazinehole_0_0", + "sample_id": null, + "children": [ + "zi_dan_jia7_jipian_0" + ], + "parent": "zi_dan_jia7", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia7_jipian_0", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia7_clipmagazinehole_0_0" + } + ] + } + }, + { + "id": "zi_dan_jia7_jipian_0", + "name": "zi_dan_jia7_jipian_0", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia7_clipmagazinehole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia7_clipmagazinehole_0_1", + "name": "zi_dan_jia7_clipmagazinehole_0_1", + "sample_id": null, + "children": [ + "zi_dan_jia7_jipian_1" + ], + "parent": "zi_dan_jia7", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia7_jipian_1", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia7_clipmagazinehole_0_1" + } + ] + } + }, + { + "id": "zi_dan_jia7_jipian_1", + "name": "zi_dan_jia7_jipian_1", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia7_clipmagazinehole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia7_clipmagazinehole_1_0", + "name": "zi_dan_jia7_clipmagazinehole_1_0", + "sample_id": null, + "children": [ + "zi_dan_jia7_jipian_2" + ], + "parent": "zi_dan_jia7", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia7_jipian_2", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia7_clipmagazinehole_1_0" + } + ] + } + }, + { + "id": "zi_dan_jia7_jipian_2", + "name": "zi_dan_jia7_jipian_2", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia7_clipmagazinehole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia7_clipmagazinehole_1_1", + "name": "zi_dan_jia7_clipmagazinehole_1_1", + "sample_id": null, + "children": [ + "zi_dan_jia7_jipian_3" + ], + "parent": "zi_dan_jia7", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia7_jipian_3", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia7_clipmagazinehole_1_1" + } + ] + } + }, + { + "id": "zi_dan_jia7_jipian_3", + "name": "zi_dan_jia7_jipian_3", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia7_clipmagazinehole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia7_clipmagazinehole_2_0", + "name": "zi_dan_jia7_clipmagazinehole_2_0", + "sample_id": null, + "children": [ + "zi_dan_jia7_jipian_4" + ], + "parent": "zi_dan_jia7", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia7_jipian_4", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia7_clipmagazinehole_2_0" + } + ] + } + }, + { + "id": "zi_dan_jia7_jipian_4", + "name": "zi_dan_jia7_jipian_4", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia7_clipmagazinehole_2_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia7_clipmagazinehole_2_1", + "name": "zi_dan_jia7_clipmagazinehole_2_1", + "sample_id": null, + "children": [ + "zi_dan_jia7_jipian_5" + ], + "parent": "zi_dan_jia7", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia7_jipian_5", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia7_clipmagazinehole_2_1" + } + ] + } + }, + { + "id": "zi_dan_jia7_jipian_5", + "name": "zi_dan_jia7_jipian_5", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia7_clipmagazinehole_2_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia8", + "name": "zi_dan_jia8", + "sample_id": null, + "children": [ + "zi_dan_jia8_clipmagazinehole_0_0", + "zi_dan_jia8_clipmagazinehole_0_1", + "zi_dan_jia8_clipmagazinehole_1_0", + "zi_dan_jia8_clipmagazinehole_1_1", + "zi_dan_jia8_clipmagazinehole_2_0", + "zi_dan_jia8_clipmagazinehole_2_1" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1280, + "y": 400, + "z": 0 + }, + "config": { + "type": "ClipMagazine", + "size_x": 80, + "size_y": 80, + "size_z": 10, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine", + "model": null, + "barcode": null, + "ordering": { + "A1": "zi_dan_jia8_clipmagazinehole_0_0", + "B1": "zi_dan_jia8_clipmagazinehole_0_1", + "A2": "zi_dan_jia8_clipmagazinehole_1_0", + "B2": "zi_dan_jia8_clipmagazinehole_1_1", + "A3": "zi_dan_jia8_clipmagazinehole_2_0", + "B3": "zi_dan_jia8_clipmagazinehole_2_1" + }, + "hole_diameter": 14.0, + "hole_depth": 10.0, + "max_sheets_per_hole": 100 + }, + "data": {} + }, + { + "id": "zi_dan_jia8_clipmagazinehole_0_0", + "name": "zi_dan_jia8_clipmagazinehole_0_0", + "sample_id": null, + "children": [ + "zi_dan_jia8_jipian_0" + ], + "parent": "zi_dan_jia8", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia8_jipian_0", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia8_clipmagazinehole_0_0" + } + ] + } + }, + { + "id": "zi_dan_jia8_jipian_0", + "name": "zi_dan_jia8_jipian_0", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia8_clipmagazinehole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia8_clipmagazinehole_0_1", + "name": "zi_dan_jia8_clipmagazinehole_0_1", + "sample_id": null, + "children": [ + "zi_dan_jia8_jipian_1" + ], + "parent": "zi_dan_jia8", + "type": "container", + "class": "", + "position": { + "x": 15.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia8_jipian_1", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia8_clipmagazinehole_0_1" + } + ] + } + }, + { + "id": "zi_dan_jia8_jipian_1", + "name": "zi_dan_jia8_jipian_1", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia8_clipmagazinehole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia8_clipmagazinehole_1_0", + "name": "zi_dan_jia8_clipmagazinehole_1_0", + "sample_id": null, + "children": [ + "zi_dan_jia8_jipian_2" + ], + "parent": "zi_dan_jia8", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia8_jipian_2", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia8_clipmagazinehole_1_0" + } + ] + } + }, + { + "id": "zi_dan_jia8_jipian_2", + "name": "zi_dan_jia8_jipian_2", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia8_clipmagazinehole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia8_clipmagazinehole_1_1", + "name": "zi_dan_jia8_clipmagazinehole_1_1", + "sample_id": null, + "children": [ + "zi_dan_jia8_jipian_3" + ], + "parent": "zi_dan_jia8", + "type": "container", + "class": "", + "position": { + "x": 40.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia8_jipian_3", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia8_clipmagazinehole_1_1" + } + ] + } + }, + { + "id": "zi_dan_jia8_jipian_3", + "name": "zi_dan_jia8_jipian_3", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia8_clipmagazinehole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia8_clipmagazinehole_2_0", + "name": "zi_dan_jia8_clipmagazinehole_2_0", + "sample_id": null, + "children": [ + "zi_dan_jia8_jipian_4" + ], + "parent": "zi_dan_jia8", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 52.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia8_jipian_4", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia8_clipmagazinehole_2_0" + } + ] + } + }, + { + "id": "zi_dan_jia8_jipian_4", + "name": "zi_dan_jia8_jipian_4", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia8_clipmagazinehole_2_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "zi_dan_jia8_clipmagazinehole_2_1", + "name": "zi_dan_jia8_clipmagazinehole_2_1", + "sample_id": null, + "children": [ + "zi_dan_jia8_jipian_5" + ], + "parent": "zi_dan_jia8", + "type": "container", + "class": "", + "position": { + "x": 65.0, + "y": 27.5, + "z": 10 + }, + "config": { + "type": "ClipMagazineHole", + "size_x": 14.0, + "size_y": 14.0, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "clip_magazine_hole", + "model": null, + "barcode": null, + "max_volume": 1960.0, + "material_z_thickness": null, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "sheet_count": 1, + "sheets": [ + { + "name": "zi_dan_jia8_jipian_5", + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "location": null, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null, + "children": [], + "parent_name": "zi_dan_jia8_clipmagazinehole_2_1" + } + ] + } + }, + { + "id": "zi_dan_jia8_jipian_5", + "name": "zi_dan_jia8_jipian_5", + "sample_id": null, + "children": [], + "parent": "zi_dan_jia8_clipmagazinehole_2_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1", + "name": "liaopan1", + "sample_id": null, + "children": [ + "liaopan1_materialhole_0_0", + "liaopan1_materialhole_0_1", + "liaopan1_materialhole_0_2", + "liaopan1_materialhole_0_3", + "liaopan1_materialhole_1_0", + "liaopan1_materialhole_1_1", + "liaopan1_materialhole_1_2", + "liaopan1_materialhole_1_3", + "liaopan1_materialhole_2_0", + "liaopan1_materialhole_2_1", + "liaopan1_materialhole_2_2", + "liaopan1_materialhole_2_3", + "liaopan1_materialhole_3_0", + "liaopan1_materialhole_3_1", + "liaopan1_materialhole_3_2", + "liaopan1_materialhole_3_3" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1010, + "y": 50, + "z": 0 + }, + "config": { + "type": "MaterialPlate", + "size_x": 120, + "size_y": 100, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_plate", + "model": null, + "barcode": null, + "ordering": { + "A1": "liaopan1_materialhole_0_0", + "B1": "liaopan1_materialhole_0_1", + "C1": "liaopan1_materialhole_0_2", + "D1": "liaopan1_materialhole_0_3", + "A2": "liaopan1_materialhole_1_0", + "B2": "liaopan1_materialhole_1_1", + "C2": "liaopan1_materialhole_1_2", + "D2": "liaopan1_materialhole_1_3", + "A3": "liaopan1_materialhole_2_0", + "B3": "liaopan1_materialhole_2_1", + "C3": "liaopan1_materialhole_2_2", + "D3": "liaopan1_materialhole_2_3", + "A4": "liaopan1_materialhole_3_0", + "B4": "liaopan1_materialhole_3_1", + "C4": "liaopan1_materialhole_3_2", + "D4": "liaopan1_materialhole_3_3" + } + }, + "data": {} + }, + { + "id": "liaopan1_materialhole_0_0", + "name": "liaopan1_materialhole_0_0", + "sample_id": null, + "children": [ + "liaopan1_jipian_0" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_0", + "name": "liaopan1_jipian_0", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_0_1", + "name": "liaopan1_materialhole_0_1", + "sample_id": null, + "children": [ + "liaopan1_jipian_1" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_1", + "name": "liaopan1_jipian_1", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_0_2", + "name": "liaopan1_materialhole_0_2", + "sample_id": null, + "children": [ + "liaopan1_jipian_2" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_2", + "name": "liaopan1_jipian_2", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_0_2", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_0_3", + "name": "liaopan1_materialhole_0_3", + "sample_id": null, + "children": [ + "liaopan1_jipian_3" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_3", + "name": "liaopan1_jipian_3", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_0_3", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_1_0", + "name": "liaopan1_materialhole_1_0", + "sample_id": null, + "children": [ + "liaopan1_jipian_4" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_4", + "name": "liaopan1_jipian_4", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_1_1", + "name": "liaopan1_materialhole_1_1", + "sample_id": null, + "children": [ + "liaopan1_jipian_5" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_5", + "name": "liaopan1_jipian_5", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_1_2", + "name": "liaopan1_materialhole_1_2", + "sample_id": null, + "children": [ + "liaopan1_jipian_6" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_6", + "name": "liaopan1_jipian_6", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_1_2", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_1_3", + "name": "liaopan1_materialhole_1_3", + "sample_id": null, + "children": [ + "liaopan1_jipian_7" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_7", + "name": "liaopan1_jipian_7", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_1_3", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_2_0", + "name": "liaopan1_materialhole_2_0", + "sample_id": null, + "children": [ + "liaopan1_jipian_8" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_8", + "name": "liaopan1_jipian_8", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_2_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_2_1", + "name": "liaopan1_materialhole_2_1", + "sample_id": null, + "children": [ + "liaopan1_jipian_9" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_9", + "name": "liaopan1_jipian_9", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_2_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_2_2", + "name": "liaopan1_materialhole_2_2", + "sample_id": null, + "children": [ + "liaopan1_jipian_10" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_10", + "name": "liaopan1_jipian_10", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_2_2", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_2_3", + "name": "liaopan1_materialhole_2_3", + "sample_id": null, + "children": [ + "liaopan1_jipian_11" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_11", + "name": "liaopan1_jipian_11", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_2_3", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_3_0", + "name": "liaopan1_materialhole_3_0", + "sample_id": null, + "children": [ + "liaopan1_jipian_12" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_12", + "name": "liaopan1_jipian_12", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_3_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_3_1", + "name": "liaopan1_materialhole_3_1", + "sample_id": null, + "children": [ + "liaopan1_jipian_13" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_13", + "name": "liaopan1_jipian_13", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_3_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_3_2", + "name": "liaopan1_materialhole_3_2", + "sample_id": null, + "children": [ + "liaopan1_jipian_14" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_14", + "name": "liaopan1_jipian_14", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_3_2", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan1_materialhole_3_3", + "name": "liaopan1_materialhole_3_3", + "sample_id": null, + "children": [ + "liaopan1_jipian_15" + ], + "parent": "liaopan1", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_jipian_15", + "name": "liaopan1_jipian_15", + "sample_id": null, + "children": [], + "parent": "liaopan1_materialhole_3_3", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan2", + "name": "liaopan2", + "sample_id": null, + "children": [ + "liaopan2_materialhole_0_0", + "liaopan2_materialhole_0_1", + "liaopan2_materialhole_0_2", + "liaopan2_materialhole_0_3", + "liaopan2_materialhole_1_0", + "liaopan2_materialhole_1_1", + "liaopan2_materialhole_1_2", + "liaopan2_materialhole_1_3", + "liaopan2_materialhole_2_0", + "liaopan2_materialhole_2_1", + "liaopan2_materialhole_2_2", + "liaopan2_materialhole_2_3", + "liaopan2_materialhole_3_0", + "liaopan2_materialhole_3_1", + "liaopan2_materialhole_3_2", + "liaopan2_materialhole_3_3" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1130, + "y": 50, + "z": 0 + }, + "config": { + "type": "MaterialPlate", + "size_x": 120, + "size_y": 100, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_plate", + "model": null, + "barcode": null, + "ordering": { + "A1": "liaopan2_materialhole_0_0", + "B1": "liaopan2_materialhole_0_1", + "C1": "liaopan2_materialhole_0_2", + "D1": "liaopan2_materialhole_0_3", + "A2": "liaopan2_materialhole_1_0", + "B2": "liaopan2_materialhole_1_1", + "C2": "liaopan2_materialhole_1_2", + "D2": "liaopan2_materialhole_1_3", + "A3": "liaopan2_materialhole_2_0", + "B3": "liaopan2_materialhole_2_1", + "C3": "liaopan2_materialhole_2_2", + "D3": "liaopan2_materialhole_2_3", + "A4": "liaopan2_materialhole_3_0", + "B4": "liaopan2_materialhole_3_1", + "C4": "liaopan2_materialhole_3_2", + "D4": "liaopan2_materialhole_3_3" + } + }, + "data": {} + }, + { + "id": "liaopan2_materialhole_0_0", + "name": "liaopan2_materialhole_0_0", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_0_1", + "name": "liaopan2_materialhole_0_1", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_0_2", + "name": "liaopan2_materialhole_0_2", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_0_3", + "name": "liaopan2_materialhole_0_3", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_1_0", + "name": "liaopan2_materialhole_1_0", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_1_1", + "name": "liaopan2_materialhole_1_1", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_1_2", + "name": "liaopan2_materialhole_1_2", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_1_3", + "name": "liaopan2_materialhole_1_3", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_2_0", + "name": "liaopan2_materialhole_2_0", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_2_1", + "name": "liaopan2_materialhole_2_1", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_2_2", + "name": "liaopan2_materialhole_2_2", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_2_3", + "name": "liaopan2_materialhole_2_3", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_3_0", + "name": "liaopan2_materialhole_3_0", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_3_1", + "name": "liaopan2_materialhole_3_1", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_3_2", + "name": "liaopan2_materialhole_3_2", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2_materialhole_3_3", + "name": "liaopan2_materialhole_3_3", + "sample_id": null, + "children": [], + "parent": "liaopan2", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3", + "name": "liaopan3", + "sample_id": null, + "children": [ + "liaopan3_materialhole_0_0", + "liaopan3_materialhole_0_1", + "liaopan3_materialhole_0_2", + "liaopan3_materialhole_0_3", + "liaopan3_materialhole_1_0", + "liaopan3_materialhole_1_1", + "liaopan3_materialhole_1_2", + "liaopan3_materialhole_1_3", + "liaopan3_materialhole_2_0", + "liaopan3_materialhole_2_1", + "liaopan3_materialhole_2_2", + "liaopan3_materialhole_2_3", + "liaopan3_materialhole_3_0", + "liaopan3_materialhole_3_1", + "liaopan3_materialhole_3_2", + "liaopan3_materialhole_3_3" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1250, + "y": 50, + "z": 0 + }, + "config": { + "type": "MaterialPlate", + "size_x": 120, + "size_y": 100, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_plate", + "model": null, + "barcode": null, + "ordering": { + "A1": "liaopan3_materialhole_0_0", + "B1": "liaopan3_materialhole_0_1", + "C1": "liaopan3_materialhole_0_2", + "D1": "liaopan3_materialhole_0_3", + "A2": "liaopan3_materialhole_1_0", + "B2": "liaopan3_materialhole_1_1", + "C2": "liaopan3_materialhole_1_2", + "D2": "liaopan3_materialhole_1_3", + "A3": "liaopan3_materialhole_2_0", + "B3": "liaopan3_materialhole_2_1", + "C3": "liaopan3_materialhole_2_2", + "D3": "liaopan3_materialhole_2_3", + "A4": "liaopan3_materialhole_3_0", + "B4": "liaopan3_materialhole_3_1", + "C4": "liaopan3_materialhole_3_2", + "D4": "liaopan3_materialhole_3_3" + } + }, + "data": {} + }, + { + "id": "liaopan3_materialhole_0_0", + "name": "liaopan3_materialhole_0_0", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_0_1", + "name": "liaopan3_materialhole_0_1", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_0_2", + "name": "liaopan3_materialhole_0_2", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_0_3", + "name": "liaopan3_materialhole_0_3", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_1_0", + "name": "liaopan3_materialhole_1_0", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_1_1", + "name": "liaopan3_materialhole_1_1", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_1_2", + "name": "liaopan3_materialhole_1_2", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_1_3", + "name": "liaopan3_materialhole_1_3", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_2_0", + "name": "liaopan3_materialhole_2_0", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_2_1", + "name": "liaopan3_materialhole_2_1", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_2_2", + "name": "liaopan3_materialhole_2_2", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_2_3", + "name": "liaopan3_materialhole_2_3", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_3_0", + "name": "liaopan3_materialhole_3_0", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_3_1", + "name": "liaopan3_materialhole_3_1", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_3_2", + "name": "liaopan3_materialhole_3_2", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan3_materialhole_3_3", + "name": "liaopan3_materialhole_3_3", + "sample_id": null, + "children": [], + "parent": "liaopan3", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4", + "name": "liaopan4", + "sample_id": null, + "children": [ + "liaopan4_materialhole_0_0", + "liaopan4_materialhole_0_1", + "liaopan4_materialhole_0_2", + "liaopan4_materialhole_0_3", + "liaopan4_materialhole_1_0", + "liaopan4_materialhole_1_1", + "liaopan4_materialhole_1_2", + "liaopan4_materialhole_1_3", + "liaopan4_materialhole_2_0", + "liaopan4_materialhole_2_1", + "liaopan4_materialhole_2_2", + "liaopan4_materialhole_2_3", + "liaopan4_materialhole_3_0", + "liaopan4_materialhole_3_1", + "liaopan4_materialhole_3_2", + "liaopan4_materialhole_3_3" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1010, + "y": 150, + "z": 0 + }, + "config": { + "type": "MaterialPlate", + "size_x": 120, + "size_y": 100, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_plate", + "model": null, + "barcode": null, + "ordering": { + "A1": "liaopan4_materialhole_0_0", + "B1": "liaopan4_materialhole_0_1", + "C1": "liaopan4_materialhole_0_2", + "D1": "liaopan4_materialhole_0_3", + "A2": "liaopan4_materialhole_1_0", + "B2": "liaopan4_materialhole_1_1", + "C2": "liaopan4_materialhole_1_2", + "D2": "liaopan4_materialhole_1_3", + "A3": "liaopan4_materialhole_2_0", + "B3": "liaopan4_materialhole_2_1", + "C3": "liaopan4_materialhole_2_2", + "D3": "liaopan4_materialhole_2_3", + "A4": "liaopan4_materialhole_3_0", + "B4": "liaopan4_materialhole_3_1", + "C4": "liaopan4_materialhole_3_2", + "D4": "liaopan4_materialhole_3_3" + } + }, + "data": {} + }, + { + "id": "liaopan4_materialhole_0_0", + "name": "liaopan4_materialhole_0_0", + "sample_id": null, + "children": [ + "liaopan4_jipian_0" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_0", + "name": "liaopan4_jipian_0", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_0_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_0_1", + "name": "liaopan4_materialhole_0_1", + "sample_id": null, + "children": [ + "liaopan4_jipian_1" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_1", + "name": "liaopan4_jipian_1", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_0_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_0_2", + "name": "liaopan4_materialhole_0_2", + "sample_id": null, + "children": [ + "liaopan4_jipian_2" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_2", + "name": "liaopan4_jipian_2", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_0_2", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_0_3", + "name": "liaopan4_materialhole_0_3", + "sample_id": null, + "children": [ + "liaopan4_jipian_3" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_3", + "name": "liaopan4_jipian_3", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_0_3", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_1_0", + "name": "liaopan4_materialhole_1_0", + "sample_id": null, + "children": [ + "liaopan4_jipian_4" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_4", + "name": "liaopan4_jipian_4", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_1_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_1_1", + "name": "liaopan4_materialhole_1_1", + "sample_id": null, + "children": [ + "liaopan4_jipian_5" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_5", + "name": "liaopan4_jipian_5", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_1_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_1_2", + "name": "liaopan4_materialhole_1_2", + "sample_id": null, + "children": [ + "liaopan4_jipian_6" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_6", + "name": "liaopan4_jipian_6", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_1_2", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_1_3", + "name": "liaopan4_materialhole_1_3", + "sample_id": null, + "children": [ + "liaopan4_jipian_7" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_7", + "name": "liaopan4_jipian_7", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_1_3", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_2_0", + "name": "liaopan4_materialhole_2_0", + "sample_id": null, + "children": [ + "liaopan4_jipian_8" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_8", + "name": "liaopan4_jipian_8", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_2_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_2_1", + "name": "liaopan4_materialhole_2_1", + "sample_id": null, + "children": [ + "liaopan4_jipian_9" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_9", + "name": "liaopan4_jipian_9", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_2_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_2_2", + "name": "liaopan4_materialhole_2_2", + "sample_id": null, + "children": [ + "liaopan4_jipian_10" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_10", + "name": "liaopan4_jipian_10", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_2_2", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_2_3", + "name": "liaopan4_materialhole_2_3", + "sample_id": null, + "children": [ + "liaopan4_jipian_11" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_11", + "name": "liaopan4_jipian_11", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_2_3", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_3_0", + "name": "liaopan4_materialhole_3_0", + "sample_id": null, + "children": [ + "liaopan4_jipian_12" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_12", + "name": "liaopan4_jipian_12", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_3_0", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_3_1", + "name": "liaopan4_materialhole_3_1", + "sample_id": null, + "children": [ + "liaopan4_jipian_13" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_13", + "name": "liaopan4_jipian_13", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_3_1", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_3_2", + "name": "liaopan4_materialhole_3_2", + "sample_id": null, + "children": [ + "liaopan4_jipian_14" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_14", + "name": "liaopan4_jipian_14", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_3_2", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan4_materialhole_3_3", + "name": "liaopan4_materialhole_3_3", + "sample_id": null, + "children": [ + "liaopan4_jipian_15" + ], + "parent": "liaopan4", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan4_jipian_15", + "name": "liaopan4_jipian_15", + "sample_id": null, + "children": [], + "parent": "liaopan4_materialhole_3_3", + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan5", + "name": "liaopan5", + "sample_id": null, + "children": [ + "liaopan5_materialhole_0_0", + "liaopan5_materialhole_0_1", + "liaopan5_materialhole_0_2", + "liaopan5_materialhole_0_3", + "liaopan5_materialhole_1_0", + "liaopan5_materialhole_1_1", + "liaopan5_materialhole_1_2", + "liaopan5_materialhole_1_3", + "liaopan5_materialhole_2_0", + "liaopan5_materialhole_2_1", + "liaopan5_materialhole_2_2", + "liaopan5_materialhole_2_3", + "liaopan5_materialhole_3_0", + "liaopan5_materialhole_3_1", + "liaopan5_materialhole_3_2", + "liaopan5_materialhole_3_3" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1130, + "y": 150, + "z": 0 + }, + "config": { + "type": "MaterialPlate", + "size_x": 120, + "size_y": 100, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_plate", + "model": null, + "barcode": null, + "ordering": { + "A1": "liaopan5_materialhole_0_0", + "B1": "liaopan5_materialhole_0_1", + "C1": "liaopan5_materialhole_0_2", + "D1": "liaopan5_materialhole_0_3", + "A2": "liaopan5_materialhole_1_0", + "B2": "liaopan5_materialhole_1_1", + "C2": "liaopan5_materialhole_1_2", + "D2": "liaopan5_materialhole_1_3", + "A3": "liaopan5_materialhole_2_0", + "B3": "liaopan5_materialhole_2_1", + "C3": "liaopan5_materialhole_2_2", + "D3": "liaopan5_materialhole_2_3", + "A4": "liaopan5_materialhole_3_0", + "B4": "liaopan5_materialhole_3_1", + "C4": "liaopan5_materialhole_3_2", + "D4": "liaopan5_materialhole_3_3" + } + }, + "data": {} + }, + { + "id": "liaopan5_materialhole_0_0", + "name": "liaopan5_materialhole_0_0", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_0_1", + "name": "liaopan5_materialhole_0_1", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_0_2", + "name": "liaopan5_materialhole_0_2", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_0_3", + "name": "liaopan5_materialhole_0_3", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_1_0", + "name": "liaopan5_materialhole_1_0", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_1_1", + "name": "liaopan5_materialhole_1_1", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_1_2", + "name": "liaopan5_materialhole_1_2", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_1_3", + "name": "liaopan5_materialhole_1_3", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_2_0", + "name": "liaopan5_materialhole_2_0", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_2_1", + "name": "liaopan5_materialhole_2_1", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_2_2", + "name": "liaopan5_materialhole_2_2", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_2_3", + "name": "liaopan5_materialhole_2_3", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_3_0", + "name": "liaopan5_materialhole_3_0", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_3_1", + "name": "liaopan5_materialhole_3_1", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_3_2", + "name": "liaopan5_materialhole_3_2", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan5_materialhole_3_3", + "name": "liaopan5_materialhole_3_3", + "sample_id": null, + "children": [], + "parent": "liaopan5", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6", + "name": "liaopan6", + "sample_id": null, + "children": [ + "liaopan6_materialhole_0_0", + "liaopan6_materialhole_0_1", + "liaopan6_materialhole_0_2", + "liaopan6_materialhole_0_3", + "liaopan6_materialhole_1_0", + "liaopan6_materialhole_1_1", + "liaopan6_materialhole_1_2", + "liaopan6_materialhole_1_3", + "liaopan6_materialhole_2_0", + "liaopan6_materialhole_2_1", + "liaopan6_materialhole_2_2", + "liaopan6_materialhole_2_3", + "liaopan6_materialhole_3_0", + "liaopan6_materialhole_3_1", + "liaopan6_materialhole_3_2", + "liaopan6_materialhole_3_3" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 1250, + "y": 150, + "z": 0 + }, + "config": { + "type": "MaterialPlate", + "size_x": 120, + "size_y": 100, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_plate", + "model": null, + "barcode": null, + "ordering": { + "A1": "liaopan6_materialhole_0_0", + "B1": "liaopan6_materialhole_0_1", + "C1": "liaopan6_materialhole_0_2", + "D1": "liaopan6_materialhole_0_3", + "A2": "liaopan6_materialhole_1_0", + "B2": "liaopan6_materialhole_1_1", + "C2": "liaopan6_materialhole_1_2", + "D2": "liaopan6_materialhole_1_3", + "A3": "liaopan6_materialhole_2_0", + "B3": "liaopan6_materialhole_2_1", + "C3": "liaopan6_materialhole_2_2", + "D3": "liaopan6_materialhole_2_3", + "A4": "liaopan6_materialhole_3_0", + "B4": "liaopan6_materialhole_3_1", + "C4": "liaopan6_materialhole_3_2", + "D4": "liaopan6_materialhole_3_3" + } + }, + "data": {} + }, + { + "id": "liaopan6_materialhole_0_0", + "name": "liaopan6_materialhole_0_0", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_0_1", + "name": "liaopan6_materialhole_0_1", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_0_2", + "name": "liaopan6_materialhole_0_2", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_0_3", + "name": "liaopan6_materialhole_0_3", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 12.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_1_0", + "name": "liaopan6_materialhole_1_0", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_1_1", + "name": "liaopan6_materialhole_1_1", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_1_2", + "name": "liaopan6_materialhole_1_2", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_1_3", + "name": "liaopan6_materialhole_1_3", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 36.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_2_0", + "name": "liaopan6_materialhole_2_0", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_2_1", + "name": "liaopan6_materialhole_2_1", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_2_2", + "name": "liaopan6_materialhole_2_2", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_2_3", + "name": "liaopan6_materialhole_2_3", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 60.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_3_0", + "name": "liaopan6_materialhole_3_0", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 74.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_3_1", + "name": "liaopan6_materialhole_3_1", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 50.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_3_2", + "name": "liaopan6_materialhole_3_2", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 26.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan6_materialhole_3_3", + "name": "liaopan6_materialhole_3_3", + "sample_id": null, + "children": [], + "parent": "liaopan6", + "type": "container", + "class": "", + "position": { + "x": 84.0, + "y": 2.0, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "bottle_rack_3x4", + "name": "bottle_rack_3x4", + "sample_id": null, + "children": [ + "sheet_3x4_0", + "sheet_3x4_1", + "sheet_3x4_2", + "sheet_3x4_3", + "sheet_3x4_4", + "sheet_3x4_5", + "sheet_3x4_6", + "sheet_3x4_7", + "sheet_3x4_8", + "sheet_3x4_9", + "sheet_3x4_10", + "sheet_3x4_11" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 100, + "y": 200, + "z": 0 + }, + "config": { + "type": "BottleRack", + "size_x": 210.0, + "size_y": 140.0, + "size_z": 100.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "bottle_rack", + "model": null, + "barcode": null, + "num_items_x": 3, + "num_items_y": 4, + "position_spacing": 35.0, + "orientation": "vertical", + "padding_x": 20.0, + "padding_y": 20.0 + }, + "data": { + "bottle_diameter": 30.0, + "bottle_height": 100.0, + "position_spacing": 35.0, + "name_to_index": {} + } + }, + { + "id": "sheet_3x4_0", + "name": "sheet_3x4_0", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 20.0, + "y": 20.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_1", + "name": "sheet_3x4_1", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 20.0, + "y": 55.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_2", + "name": "sheet_3x4_2", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 20.0, + "y": 90.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_3", + "name": "sheet_3x4_3", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 55.0, + "y": 20.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_4", + "name": "sheet_3x4_4", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 55.0, + "y": 55.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_5", + "name": "sheet_3x4_5", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 55.0, + "y": 90.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_6", + "name": "sheet_3x4_6", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 90.0, + "y": 20.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_7", + "name": "sheet_3x4_7", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 90.0, + "y": 55.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_8", + "name": "sheet_3x4_8", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 90.0, + "y": 90.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_9", + "name": "sheet_3x4_9", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 125.0, + "y": 20.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_10", + "name": "sheet_3x4_10", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 125.0, + "y": 55.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_3x4_11", + "name": "sheet_3x4_11", + "sample_id": null, + "children": [], + "parent": "bottle_rack_3x4", + "type": "container", + "class": "", + "position": { + "x": 125.0, + "y": 90.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "bottle_rack_6x2", + "name": "bottle_rack_6x2", + "sample_id": null, + "children": [ + "sheet_6x2_0", + "sheet_6x2_1", + "sheet_6x2_2", + "sheet_6x2_3", + "sheet_6x2_4", + "sheet_6x2_5", + "sheet_6x2_6", + "sheet_6x2_7", + "sheet_6x2_8", + "sheet_6x2_9", + "sheet_6x2_10", + "sheet_6x2_11" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 300, + "y": 300, + "z": 0 + }, + "config": { + "type": "BottleRack", + "size_x": 120.0, + "size_y": 250.0, + "size_z": 100.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "bottle_rack", + "model": null, + "barcode": null, + "num_items_x": 6, + "num_items_y": 2, + "position_spacing": 35.0, + "orientation": "vertical", + "padding_x": 20.0, + "padding_y": 20.0 + }, + "data": { + "bottle_diameter": 30.0, + "bottle_height": 100.0, + "position_spacing": 35.0, + "name_to_index": {} + } + }, + { + "id": "sheet_6x2_0", + "name": "sheet_6x2_0", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 20.0, + "y": 20.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_1", + "name": "sheet_6x2_1", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 20.0, + "y": 55.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_2", + "name": "sheet_6x2_2", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 20.0, + "y": 90.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_3", + "name": "sheet_6x2_3", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 20.0, + "y": 125.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_4", + "name": "sheet_6x2_4", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 20.0, + "y": 160.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_5", + "name": "sheet_6x2_5", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 20.0, + "y": 195.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_6", + "name": "sheet_6x2_6", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 55.0, + "y": 20.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_7", + "name": "sheet_6x2_7", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 55.0, + "y": 55.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_8", + "name": "sheet_6x2_8", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 55.0, + "y": 90.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_9", + "name": "sheet_6x2_9", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 55.0, + "y": 125.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_10", + "name": "sheet_6x2_10", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 55.0, + "y": 160.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "sheet_6x2_11", + "name": "sheet_6x2_11", + "sample_id": null, + "children": [], + "parent": "bottle_rack_6x2", + "type": "container", + "class": "", + "position": { + "x": 55.0, + "y": 195.0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "bottle_rack_6x2_2", + "name": "bottle_rack_6x2_2", + "sample_id": null, + "children": [], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 430, + "y": 300, + "z": 0 + }, + "config": { + "type": "BottleRack", + "size_x": 120.0, + "size_y": 250.0, + "size_z": 100.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "bottle_rack", + "model": null, + "barcode": null, + "num_items_x": 6, + "num_items_y": 2, + "position_spacing": 35.0, + "orientation": "vertical", + "padding_x": 20.0, + "padding_y": 20.0 + }, + "data": { + "bottle_diameter": 30.0, + "bottle_height": 100.0, + "position_spacing": 35.0, + "name_to_index": {} + } + }, + { + "id": "tip_box_64", + "name": "tip_box_64", + "sample_id": null, + "children": [ + "tip_box_64_tipspot_0_0", + "tip_box_64_tipspot_0_1", + "tip_box_64_tipspot_0_2", + "tip_box_64_tipspot_0_3", + "tip_box_64_tipspot_0_4", + "tip_box_64_tipspot_0_5", + "tip_box_64_tipspot_0_6", + "tip_box_64_tipspot_0_7", + "tip_box_64_tipspot_1_0", + "tip_box_64_tipspot_1_1", + "tip_box_64_tipspot_1_2", + "tip_box_64_tipspot_1_3", + "tip_box_64_tipspot_1_4", + "tip_box_64_tipspot_1_5", + "tip_box_64_tipspot_1_6", + "tip_box_64_tipspot_1_7", + "tip_box_64_tipspot_2_0", + "tip_box_64_tipspot_2_1", + "tip_box_64_tipspot_2_2", + "tip_box_64_tipspot_2_3", + "tip_box_64_tipspot_2_4", + "tip_box_64_tipspot_2_5", + "tip_box_64_tipspot_2_6", + "tip_box_64_tipspot_2_7", + "tip_box_64_tipspot_3_0", + "tip_box_64_tipspot_3_1", + "tip_box_64_tipspot_3_2", + "tip_box_64_tipspot_3_3", + "tip_box_64_tipspot_3_4", + "tip_box_64_tipspot_3_5", + "tip_box_64_tipspot_3_6", + "tip_box_64_tipspot_3_7", + "tip_box_64_tipspot_4_0", + "tip_box_64_tipspot_4_1", + "tip_box_64_tipspot_4_2", + "tip_box_64_tipspot_4_3", + "tip_box_64_tipspot_4_4", + "tip_box_64_tipspot_4_5", + "tip_box_64_tipspot_4_6", + "tip_box_64_tipspot_4_7", + "tip_box_64_tipspot_5_0", + "tip_box_64_tipspot_5_1", + "tip_box_64_tipspot_5_2", + "tip_box_64_tipspot_5_3", + "tip_box_64_tipspot_5_4", + "tip_box_64_tipspot_5_5", + "tip_box_64_tipspot_5_6", + "tip_box_64_tipspot_5_7", + "tip_box_64_tipspot_6_0", + "tip_box_64_tipspot_6_1", + "tip_box_64_tipspot_6_2", + "tip_box_64_tipspot_6_3", + "tip_box_64_tipspot_6_4", + "tip_box_64_tipspot_6_5", + "tip_box_64_tipspot_6_6", + "tip_box_64_tipspot_6_7", + "tip_box_64_tipspot_7_0", + "tip_box_64_tipspot_7_1", + "tip_box_64_tipspot_7_2", + "tip_box_64_tipspot_7_3", + "tip_box_64_tipspot_7_4", + "tip_box_64_tipspot_7_5", + "tip_box_64_tipspot_7_6", + "tip_box_64_tipspot_7_7" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 300, + "y": 100, + "z": 0 + }, + "config": { + "type": "TipBox64", + "size_x": 127.8, + "size_y": 85.5, + "size_z": 60.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_box_64", + "model": null, + "barcode": null, + "ordering": { + "A1": "tip_box_64_tipspot_0_0", + "B1": "tip_box_64_tipspot_0_1", + "C1": "tip_box_64_tipspot_0_2", + "D1": "tip_box_64_tipspot_0_3", + "E1": "tip_box_64_tipspot_0_4", + "F1": "tip_box_64_tipspot_0_5", + "G1": "tip_box_64_tipspot_0_6", + "H1": "tip_box_64_tipspot_0_7", + "A2": "tip_box_64_tipspot_1_0", + "B2": "tip_box_64_tipspot_1_1", + "C2": "tip_box_64_tipspot_1_2", + "D2": "tip_box_64_tipspot_1_3", + "E2": "tip_box_64_tipspot_1_4", + "F2": "tip_box_64_tipspot_1_5", + "G2": "tip_box_64_tipspot_1_6", + "H2": "tip_box_64_tipspot_1_7", + "A3": "tip_box_64_tipspot_2_0", + "B3": "tip_box_64_tipspot_2_1", + "C3": "tip_box_64_tipspot_2_2", + "D3": "tip_box_64_tipspot_2_3", + "E3": "tip_box_64_tipspot_2_4", + "F3": "tip_box_64_tipspot_2_5", + "G3": "tip_box_64_tipspot_2_6", + "H3": "tip_box_64_tipspot_2_7", + "A4": "tip_box_64_tipspot_3_0", + "B4": "tip_box_64_tipspot_3_1", + "C4": "tip_box_64_tipspot_3_2", + "D4": "tip_box_64_tipspot_3_3", + "E4": "tip_box_64_tipspot_3_4", + "F4": "tip_box_64_tipspot_3_5", + "G4": "tip_box_64_tipspot_3_6", + "H4": "tip_box_64_tipspot_3_7", + "A5": "tip_box_64_tipspot_4_0", + "B5": "tip_box_64_tipspot_4_1", + "C5": "tip_box_64_tipspot_4_2", + "D5": "tip_box_64_tipspot_4_3", + "E5": "tip_box_64_tipspot_4_4", + "F5": "tip_box_64_tipspot_4_5", + "G5": "tip_box_64_tipspot_4_6", + "H5": "tip_box_64_tipspot_4_7", + "A6": "tip_box_64_tipspot_5_0", + "B6": "tip_box_64_tipspot_5_1", + "C6": "tip_box_64_tipspot_5_2", + "D6": "tip_box_64_tipspot_5_3", + "E6": "tip_box_64_tipspot_5_4", + "F6": "tip_box_64_tipspot_5_5", + "G6": "tip_box_64_tipspot_5_6", + "H6": "tip_box_64_tipspot_5_7", + "A7": "tip_box_64_tipspot_6_0", + "B7": "tip_box_64_tipspot_6_1", + "C7": "tip_box_64_tipspot_6_2", + "D7": "tip_box_64_tipspot_6_3", + "E7": "tip_box_64_tipspot_6_4", + "F7": "tip_box_64_tipspot_6_5", + "G7": "tip_box_64_tipspot_6_6", + "H7": "tip_box_64_tipspot_6_7", + "A8": "tip_box_64_tipspot_7_0", + "B8": "tip_box_64_tipspot_7_1", + "C8": "tip_box_64_tipspot_7_2", + "D8": "tip_box_64_tipspot_7_3", + "E8": "tip_box_64_tipspot_7_4", + "F8": "tip_box_64_tipspot_7_5", + "G8": "tip_box_64_tipspot_7_6", + "H8": "tip_box_64_tipspot_7_7" + }, + "num_items_x": 8, + "num_items_y": 8, + "dx": 8.0, + "dy": 8.0, + "item_dx": 9.0, + "item_dy": 9.0 + }, + "data": {} + }, + { + "id": "tip_box_64_tipspot_0_0", + "name": "tip_box_64_tipspot_0_0", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 8.0, + "y": 71.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_0_1", + "name": "tip_box_64_tipspot_0_1", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 8.0, + "y": 62.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_0_2", + "name": "tip_box_64_tipspot_0_2", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 8.0, + "y": 53.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_0_3", + "name": "tip_box_64_tipspot_0_3", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 8.0, + "y": 44.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_0_4", + "name": "tip_box_64_tipspot_0_4", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 8.0, + "y": 35.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_0_5", + "name": "tip_box_64_tipspot_0_5", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 8.0, + "y": 26.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_0_6", + "name": "tip_box_64_tipspot_0_6", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 8.0, + "y": 17.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_0_7", + "name": "tip_box_64_tipspot_0_7", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 8.0, + "y": 8.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_1_0", + "name": "tip_box_64_tipspot_1_0", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 17.0, + "y": 71.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_1_1", + "name": "tip_box_64_tipspot_1_1", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 17.0, + "y": 62.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_1_2", + "name": "tip_box_64_tipspot_1_2", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 17.0, + "y": 53.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_1_3", + "name": "tip_box_64_tipspot_1_3", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 17.0, + "y": 44.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_1_4", + "name": "tip_box_64_tipspot_1_4", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 17.0, + "y": 35.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_1_5", + "name": "tip_box_64_tipspot_1_5", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 17.0, + "y": 26.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_1_6", + "name": "tip_box_64_tipspot_1_6", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 17.0, + "y": 17.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_1_7", + "name": "tip_box_64_tipspot_1_7", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 17.0, + "y": 8.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_2_0", + "name": "tip_box_64_tipspot_2_0", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 26.0, + "y": 71.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_2_1", + "name": "tip_box_64_tipspot_2_1", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 26.0, + "y": 62.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_2_2", + "name": "tip_box_64_tipspot_2_2", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 26.0, + "y": 53.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_2_3", + "name": "tip_box_64_tipspot_2_3", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 26.0, + "y": 44.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_2_4", + "name": "tip_box_64_tipspot_2_4", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 26.0, + "y": 35.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_2_5", + "name": "tip_box_64_tipspot_2_5", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 26.0, + "y": 26.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_2_6", + "name": "tip_box_64_tipspot_2_6", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 26.0, + "y": 17.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_2_7", + "name": "tip_box_64_tipspot_2_7", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 26.0, + "y": 8.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_3_0", + "name": "tip_box_64_tipspot_3_0", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 35.0, + "y": 71.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_3_1", + "name": "tip_box_64_tipspot_3_1", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 35.0, + "y": 62.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_3_2", + "name": "tip_box_64_tipspot_3_2", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 35.0, + "y": 53.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_3_3", + "name": "tip_box_64_tipspot_3_3", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 35.0, + "y": 44.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_3_4", + "name": "tip_box_64_tipspot_3_4", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 35.0, + "y": 35.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_3_5", + "name": "tip_box_64_tipspot_3_5", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 35.0, + "y": 26.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_3_6", + "name": "tip_box_64_tipspot_3_6", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 35.0, + "y": 17.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_3_7", + "name": "tip_box_64_tipspot_3_7", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 35.0, + "y": 8.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_4_0", + "name": "tip_box_64_tipspot_4_0", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 44.0, + "y": 71.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_4_1", + "name": "tip_box_64_tipspot_4_1", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 44.0, + "y": 62.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_4_2", + "name": "tip_box_64_tipspot_4_2", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 44.0, + "y": 53.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_4_3", + "name": "tip_box_64_tipspot_4_3", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 44.0, + "y": 44.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_4_4", + "name": "tip_box_64_tipspot_4_4", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 44.0, + "y": 35.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_4_5", + "name": "tip_box_64_tipspot_4_5", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 44.0, + "y": 26.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_4_6", + "name": "tip_box_64_tipspot_4_6", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 44.0, + "y": 17.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_4_7", + "name": "tip_box_64_tipspot_4_7", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 44.0, + "y": 8.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_5_0", + "name": "tip_box_64_tipspot_5_0", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 53.0, + "y": 71.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_5_1", + "name": "tip_box_64_tipspot_5_1", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 53.0, + "y": 62.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_5_2", + "name": "tip_box_64_tipspot_5_2", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 53.0, + "y": 53.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_5_3", + "name": "tip_box_64_tipspot_5_3", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 53.0, + "y": 44.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_5_4", + "name": "tip_box_64_tipspot_5_4", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 53.0, + "y": 35.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_5_5", + "name": "tip_box_64_tipspot_5_5", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 53.0, + "y": 26.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_5_6", + "name": "tip_box_64_tipspot_5_6", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 53.0, + "y": 17.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_5_7", + "name": "tip_box_64_tipspot_5_7", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 53.0, + "y": 8.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_6_0", + "name": "tip_box_64_tipspot_6_0", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 62.0, + "y": 71.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_6_1", + "name": "tip_box_64_tipspot_6_1", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 62.0, + "y": 62.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_6_2", + "name": "tip_box_64_tipspot_6_2", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 62.0, + "y": 53.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_6_3", + "name": "tip_box_64_tipspot_6_3", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 62.0, + "y": 44.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_6_4", + "name": "tip_box_64_tipspot_6_4", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 62.0, + "y": 35.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_6_5", + "name": "tip_box_64_tipspot_6_5", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 62.0, + "y": 26.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_6_6", + "name": "tip_box_64_tipspot_6_6", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 62.0, + "y": 17.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_6_7", + "name": "tip_box_64_tipspot_6_7", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 62.0, + "y": 8.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_7_0", + "name": "tip_box_64_tipspot_7_0", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 71.0, + "y": 71.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_7_1", + "name": "tip_box_64_tipspot_7_1", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 71.0, + "y": 62.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_7_2", + "name": "tip_box_64_tipspot_7_2", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 71.0, + "y": 53.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_7_3", + "name": "tip_box_64_tipspot_7_3", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 71.0, + "y": 44.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_7_4", + "name": "tip_box_64_tipspot_7_4", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 71.0, + "y": 35.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_7_5", + "name": "tip_box_64_tipspot_7_5", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 71.0, + "y": 26.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_7_6", + "name": "tip_box_64_tipspot_7_6", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 71.0, + "y": 17.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "tip_box_64_tipspot_7_7", + "name": "tip_box_64_tipspot_7_7", + "sample_id": null, + "children": [], + "parent": "tip_box_64", + "type": "container", + "class": "", + "position": { + "x": 71.0, + "y": 8.0, + "z": 0.0 + }, + "config": { + "type": "TipSpot", + "size_x": 10, + "size_y": 10, + "size_z": 0.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "barcode": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 20.0, + "has_filter": false, + "maximal_volume": 1000, + "fitting_depth": 8.0 + } + } + }, + { + "id": "waste_tip_box", + "name": "waste_tip_box", + "sample_id": null, + "children": [], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 300, + "y": 200, + "z": 0 + }, + "config": { + "type": "WasteTipBox", + "size_x": 127.8, + "size_y": 85.5, + "size_z": 60.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "waste_tip_box", + "model": null, + "barcode": null, + "max_volume": "Infinity", + "material_z_thickness": 0, + "compute_volume_from_height": null, + "compute_height_from_volume": null + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + } + ], + "links": [] +} \ No newline at end of file From 34f05f2e2552a4b7359ae7f4002053900e3fe0fc Mon Sep 17 00:00:00 2001 From: Junhan Chang Date: Wed, 24 Sep 2025 10:53:11 +0800 Subject: [PATCH 02/17] refactor: rename "station_resource" to "deck" --- .../comprehensive_slim.json | 2 +- .../button_battery_station.py | 2 +- .../coin_cell_assembly/coin_cell_assembly.py | 32 +++++++++---------- .../coin_cell_assembly/new_cellconfig4.json | 4 +-- .../devices/workstation/workstation_base.py | 10 +++--- unilabos/registry/devices/work_station.yaml | 4 +-- unilabos/ros/utils/driver_creator.py | 10 +++--- 7 files changed, 32 insertions(+), 32 deletions(-) diff --git a/test/experiments/comprehensive_protocol/comprehensive_slim.json b/test/experiments/comprehensive_protocol/comprehensive_slim.json index f533d22b..83e15679 100644 --- a/test/experiments/comprehensive_protocol/comprehensive_slim.json +++ b/test/experiments/comprehensive_protocol/comprehensive_slim.json @@ -41,7 +41,7 @@ "HydrogenateProtocol", "RecrystallizeProtocol" ], - "station_resource": { + "deck": { "data": { "_resource_child_name": "deck", "_resource_type": "pylabrobot.resources.opentrons.deck:OTDeck" diff --git a/unilabos/devices/workstation/coin_cell_assembly/button_battery_station.py b/unilabos/devices/workstation/coin_cell_assembly/button_battery_station.py index 676141aa..ffce04be 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/button_battery_station.py +++ b/unilabos/devices/workstation/coin_cell_assembly/button_battery_station.py @@ -1283,7 +1283,7 @@ if __name__ == "__main__": # 检查序列化后的资源 - json.dump({"nodes": resources, "links": []}, open("button_battery_station_resources_unilab.json", "w"), indent=2) + json.dump({"nodes": resources, "links": []}, open("button_battery_decks_unilab.json", "w"), indent=2) #print(resources) diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py index 1fefd8ce..4b8ba73d 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py @@ -21,7 +21,7 @@ from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode class CoinCellAssemblyWorkstation(WorkstationBase): def __init__( self, - station_resource: CoincellDeck, + deck: CoincellDeck, address: str = "192.168.1.20", port: str = "502", debug_mode: bool = True, @@ -30,12 +30,12 @@ class CoinCellAssemblyWorkstation(WorkstationBase): ): super().__init__( #桌子 - station_resource=station_resource, + deck=deck, *args, **kwargs, ) self.debug_mode = debug_mode - self.station_resource = station_resource + self.deck = deck """ 连接初始化 """ modbus_client = TCPClient(addr=address, port=port) print("modbus_client", modbus_client) @@ -74,7 +74,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase): self._ros_node = ros_node #self.deck = create_a_coin_cell_deck() ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ - "resources": [self.station_resource] + "resources": [self.deck] }) # 批量操作在这里写 @@ -84,7 +84,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase): async def fill_plate(self): - plate_1: MaterialPlate = self.station_resource.children[0].children[0] + plate_1: MaterialPlate = self.deck.children[0].children[0] #plate_1 return await self._ros_node.update_resource(plate_1) @@ -341,7 +341,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase): def modify_deck_name(self, resource_name: str): # figure_res = self._ros_node.resource_tracker.figure_resource({"name": resource_name}) # print(f"!!! figure_res: {type(figure_res)}") - self.station_resource.children[1] + self.deck.children[1] return @property @@ -814,8 +814,8 @@ class CoinCellAssemblyWorkstation(WorkstationBase): #这里定义物料系统 # TODO:读完再将电池数加一还是进入循环就将电池数加一需要考虑 - liaopan1 = self.station_resource.get_resource("liaopan1") - liaopan4 = self.station_resource.get_resource("liaopan4") + liaopan1 = self.deck.get_resource("liaopan1") + liaopan4 = self.deck.get_resource("liaopan4") jipian1 = liaopan1.children[coin_num_N].children[0] jipian4 = liaopan4.children[coin_num_N].children[0] #print(jipian1) @@ -828,7 +828,7 @@ class CoinCellAssemblyWorkstation(WorkstationBase): battery.assign_child_resource(jipian1, location=None) battery.assign_child_resource(jipian4, location=None) - zidanjia6 = self.station_resource.get_resource("zi_dan_jia6") + zidanjia6 = self.deck.get_resource("zi_dan_jia6") zidanjia6.children[0].assign_child_resource(battery, location=None) @@ -878,11 +878,11 @@ class CoinCellAssemblyWorkstation(WorkstationBase): def fun_wuliao_test(self) -> bool: #找到data_init中构建的2个物料盘 - #liaopan1 = self.station_resource.get_resource("liaopan1") - #liaopan4 = self.station_resource.get_resource("liaopan4") + #liaopan1 = self.deck.get_resource("liaopan1") + #liaopan4 = self.deck.get_resource("liaopan4") #for coin_num_N in range(16): - # liaopan1 = self.station_resource.get_resource("liaopan1") - # liaopan4 = self.station_resource.get_resource("liaopan4") + # liaopan1 = self.deck.get_resource("liaopan1") + # liaopan4 = self.deck.get_resource("liaopan4") # jipian1 = liaopan1.children[coin_num_N].children[0] # jipian4 = liaopan4.children[coin_num_N].children[0] # #print(jipian1) @@ -895,10 +895,10 @@ class CoinCellAssemblyWorkstation(WorkstationBase): # battery.assign_child_resource(jipian1, location=None) # battery.assign_child_resource(jipian4, location=None) # - # zidanjia6 = self.station_resource.get_resource("zi_dan_jia6") + # zidanjia6 = self.deck.get_resource("zi_dan_jia6") # zidanjia6.children[0].assign_child_resource(battery, location=None) # ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ - # "resources": [self.station_resource] + # "resources": [self.deck] # }) # time.sleep(2) for i in range(20): @@ -1133,7 +1133,7 @@ if __name__ == "__main__": ##print(jipian2.parent) from unilabos.resources.graphio import resource_ulab_to_plr, convert_resources_to_type - with open("./button_battery_station_resources_unilab.json", "r", encoding="utf-8") as f: + with open("./button_battery_decks_unilab.json", "r", encoding="utf-8") as f: bioyond_resources_unilab = json.load(f) print(f"成功读取 JSON 文件,包含 {len(bioyond_resources_unilab)} 个资源") ulab_resources = convert_resources_to_type(bioyond_resources_unilab, List[PLRResource]) diff --git a/unilabos/devices/workstation/coin_cell_assembly/new_cellconfig4.json b/unilabos/devices/workstation/coin_cell_assembly/new_cellconfig4.json index 0ba79b72..7e371327 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/new_cellconfig4.json +++ b/unilabos/devices/workstation/coin_cell_assembly/new_cellconfig4.json @@ -16,9 +16,9 @@ }, "config": { "debug_mode": false, - "_comment": "protocol_type接外部工站固定写法字段,一般为空,station_resource写法也固定", + "_comment": "protocol_type接外部工站固定写法字段,一般为空,deck写法也固定", "protocol_type": [], - "station_resource": { + "deck": { "data": { "_resource_child_name": "coin_cell_deck", "_resource_type": "unilabos.devices.workstation.coin_cell_assembly.button_battery_station:CoincellDeck" diff --git a/unilabos/devices/workstation/workstation_base.py b/unilabos/devices/workstation/workstation_base.py index f8cea028..62b5e657 100644 --- a/unilabos/devices/workstation/workstation_base.py +++ b/unilabos/devices/workstation/workstation_base.py @@ -147,13 +147,13 @@ class WorkstationBase(ABC): def __init__( self, - station_resource: PLRResource, + deck: PLRResource, *args, **kwargs, # 必须有kwargs ): # 基本配置 - print(station_resource) - self.deck_config = station_resource + print(deck) + self.deck_config = deck # PLR 物料系统 self.deck: Optional[Deck] = None @@ -391,5 +391,5 @@ class WorkstationBase(ABC): class ProtocolNode(WorkstationBase): - def __init__(self, station_resource: Optional[PLRResource], *args, **kwargs): - super().__init__(station_resource, *args, **kwargs) + def __init__(self, deck: Optional[PLRResource], *args, **kwargs): + super().__init__(deck, *args, **kwargs) diff --git a/unilabos/registry/devices/work_station.yaml b/unilabos/registry/devices/work_station.yaml index c5e7fced..74987ee0 100644 --- a/unilabos/registry/devices/work_station.yaml +++ b/unilabos/registry/devices/work_station.yaml @@ -6034,10 +6034,10 @@ workstation: init_param_schema: config: properties: - station_resource: + deck: type: string required: - - station_resource + - deck type: object data: properties: {} diff --git a/unilabos/ros/utils/driver_creator.py b/unilabos/ros/utils/driver_creator.py index 76c6f776..f0fdddd0 100644 --- a/unilabos/ros/utils/driver_creator.py +++ b/unilabos/ros/utils/driver_creator.py @@ -296,14 +296,14 @@ class WorkstationNodeCreator(DeviceClassCreator[T]): try: # 创建实例,额外补充一个给protocol node的字段,后面考虑取消 data["children"] = self.children - station_resource_dict = data.get("station_resource") - if station_resource_dict: + deck_dict = data.get("deck") + if deck_dict: from pylabrobot.resources import Deck, Resource plrc = PyLabRobotCreator(Deck, self.children, self.resource_tracker) - station_resource = plrc.create_instance(station_resource_dict) - data["station_resource"] = station_resource + deck = plrc.create_instance(deck_dict) + data["deck"] = deck else: - data["station_resource"] = None + data["deck"] = None self.device_instance = super(WorkstationNodeCreator, self).create_instance(data) self.post_create() return self.device_instance From a8419dc0c3ae880644fd11e53373769da277176c Mon Sep 17 00:00:00 2001 From: Junhan Chang Date: Thu, 25 Sep 2025 03:49:07 +0800 Subject: [PATCH 03/17] add standardized BIOYOND resources: bottle_carrier, bottle --- test/resources/bottle_carrier.py | 49 ++++++++++++ unilabos/resources/bioyond/__init__.py | 0 unilabos/resources/bioyond/bottle.py | 50 +++++++++++++ unilabos/resources/bioyond/bottle_carrier.py | 78 +++++++++++++++++++ unilabos/resources/bioyond/warehouse.py | 54 +++++++++++++ unilabos/resources/bottle_carrier.py | 72 ++++++++++++++++++ unilabos/resources/warehouse.py | 79 ++++++++++++++++++++ 7 files changed, 382 insertions(+) create mode 100644 test/resources/bottle_carrier.py create mode 100644 unilabos/resources/bioyond/__init__.py create mode 100644 unilabos/resources/bioyond/bottle.py create mode 100644 unilabos/resources/bioyond/bottle_carrier.py create mode 100644 unilabos/resources/bioyond/warehouse.py create mode 100644 unilabos/resources/bottle_carrier.py create mode 100644 unilabos/resources/warehouse.py diff --git a/test/resources/bottle_carrier.py b/test/resources/bottle_carrier.py new file mode 100644 index 00000000..162ead9e --- /dev/null +++ b/test/resources/bottle_carrier.py @@ -0,0 +1,49 @@ +import pytest + +from unilabos.resources.bioyond.bottle_carrier import BIOYOND_Electrolyte_6VialCarrier, BIOYOND_Electrolyte_1BottleCarrier +from unilabos.resources.bioyond.bottle import create_powder_bottle, create_solution_beaker, create_reagent_bottle + + +@pytest.fixture +def bottle_carrier() -> "BottleCarrier": + print("创建载架...") + + # 创建6瓶载架 + bottle_carrier = BIOYOND_Electrolyte_6VialCarrier("powder_carrier_01") + print(f"6瓶载架: {bottle_carrier.name}, 位置数: {len(bottle_carrier.sites)}") + + # 创建1烧杯载架 + beaker_carrier = BIOYOND_Electrolyte_1BottleCarrier("solution_carrier_01") + print(f"1烧杯载架: {beaker_carrier.name}, 位置数: {len(beaker_carrier.sites)}") + + # 创建瓶子和烧杯 + powder_bottle = create_powder_bottle("powder_bottle_01") + solution_beaker = create_solution_beaker("solution_beaker_01") + reagent_bottle = create_reagent_bottle("reagent_bottle_01") + + print(f"\n创建的物料:") + print(f"粉末瓶: {powder_bottle.name} - {powder_bottle.diameter}mm x {powder_bottle.height}mm, {powder_bottle.max_volume}μL") + print(f"溶液烧杯: {solution_beaker.name} - {solution_beaker.diameter}mm x {solution_beaker.height}mm, {solution_beaker.max_volume}μL") + print(f"试剂瓶: {reagent_bottle.name} - {reagent_bottle.diameter}mm x {reagent_bottle.height}mm, {reagent_bottle.max_volume}μL") + + # 测试放置容器 + print(f"\n测试放置容器...") + + # 通过载架的索引操作来放置容器 + bottle_carrier[0] = powder_bottle # 放置粉末瓶到第一个位置 + print(f"粉末瓶已放置到6瓶载架的位置 0") + + beaker_carrier[0] = solution_beaker # 放置烧杯到第一个位置 + print(f"溶液烧杯已放置到1烧杯载架的位置 0") + + # 验证放置结果 + print(f"\n验证放置结果:") + bottle_at_0 = bottle_carrier[0].resource + beaker_at_0 = beaker_carrier[0].resource + + if bottle_at_0: + print(f"位置 0 的瓶子: {bottle_at_0.name}") + if beaker_at_0: + print(f"位置 0 的烧杯: {beaker_at_0.name}") + + print("\n载架设置完成!") \ No newline at end of file diff --git a/unilabos/resources/bioyond/__init__.py b/unilabos/resources/bioyond/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/unilabos/resources/bioyond/bottle.py b/unilabos/resources/bioyond/bottle.py new file mode 100644 index 00000000..f642ca2e --- /dev/null +++ b/unilabos/resources/bioyond/bottle.py @@ -0,0 +1,50 @@ +from unilabos.resources.bottle_carrier import Bottle, BottleCarrier +# 工厂函数 + + +def create_powder_bottle( + name: str, + diameter: float = 30.0, + height: float = 50.0, + max_volume: float = 50000.0, # 50mL +) -> Bottle: + """创建粉末瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + category="powder_bottle", + ) + + +def create_solution_beaker( + name: str, + diameter: float = 80.0, + height: float = 100.0, + max_volume: float = 500000.0, # 500mL +) -> Bottle: + """创建溶液烧杯""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + category="solution_beaker", + ) + + +def create_reagent_bottle( + name: str, + diameter: float = 20.0, + height: float = 40.0, + max_volume: float = 15000.0, # 15mL +) -> Bottle: + """创建试剂瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + category="reagent_bottle", + ) \ No newline at end of file diff --git a/unilabos/resources/bioyond/bottle_carrier.py b/unilabos/resources/bioyond/bottle_carrier.py new file mode 100644 index 00000000..e7ac488e --- /dev/null +++ b/unilabos/resources/bioyond/bottle_carrier.py @@ -0,0 +1,78 @@ +from unilabos.resources.bottle_carrier import Bottle, BottleCarrier +from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder + + +# 命名约定:试剂瓶-Bottle,烧杯-Beaker,烧瓶-Flask,小瓶-Vial + +def BIOYOND_Electrolyte_6VialCarrier(name: str) -> BottleCarrier: + """6瓶载架 - 2x3布局""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 50.0 + + # 瓶位尺寸 + bottle_diameter = 30.0 + bottle_spacing_x = 42.0 # X方向间距 + bottle_spacing_y = 35.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + # 创建6个位置坐标 (2行 x 3列) + locations = [] + for row in range(2): + for col in range(3): + x = start_x + col * bottle_spacing_x + y = start_y + row * bottle_spacing_y + z = 5.0 # 架位底部 + locations.append(Coordinate(x, y, z)) + + return BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=create_homogeneous_resources( + klass=ResourceHolder, + locations=locations, + resource_size_x=bottle_diameter, + resource_size_y=bottle_diameter, + name_prefix=name, + ), + model="BIOYOND_Electrolyte_6VialCarrier", + ) + + +def BIOYOND_Electrolyte_1BottleCarrier(name: str) -> BottleCarrier: + """1瓶载架 - 单个中央位置""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 100.0 + + # 烧杯尺寸 + beaker_diameter = 80.0 + + # 计算中央位置 + center_x = (carrier_size_x - beaker_diameter) / 2 + center_y = (carrier_size_y - beaker_diameter) / 2 + center_z = 5.0 + + return BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=create_homogeneous_resources( + klass=ResourceHolder, + locations=[Coordinate(center_x, center_y, center_z)], + resource_size_x=beaker_diameter, + resource_size_y=beaker_diameter, + name_prefix=name, + ), + model="BIOYOND_Electrolyte_1BottleCarrier", + ) \ No newline at end of file diff --git a/unilabos/resources/bioyond/warehouse.py b/unilabos/resources/bioyond/warehouse.py new file mode 100644 index 00000000..c468f875 --- /dev/null +++ b/unilabos/resources/bioyond/warehouse.py @@ -0,0 +1,54 @@ +from unilabos.resources.warehouse import WareHouse + + +def bioyond_warehouse_1x4x4(name: str) -> WareHouse: + """创建BioYond 4x1x4仓库""" + return WareHouse( + name=name, + num_items_x=1, + num_items_y=4, + num_items_z=4, + dx=137.0, + dy=96.0, + dz=120.0, + item_dx=10.0, + item_dy=10.0, + item_dz=10.0, + category="warehouse", + ) + + +def bioyond_warehouse_1x3x2(name: str) -> WareHouse: + """创建BioYond 3x1x2仓库""" + return WareHouse( + name=name, + num_items_x=1, + num_items_y=3, + num_items_z=2, + dx=137.0, + dy=96.0, + dz=120.0, + item_dx=10.0, + item_dy=10.0, + item_dz=10.0, + category="warehouse", + removed_positions=None + ) + + +def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse: + """创建BioYond开关盖加液模块台面""" + return WareHouse( + name=name, + num_items_x=2, + num_items_y=5, + num_items_z=1, + dx=137.0, + dy=96.0, + dz=120.0, + item_dx=10.0, + item_dy=10.0, + item_dz=10.0, + category="warehouse", + removed_positions=None + ) \ No newline at end of file diff --git a/unilabos/resources/bottle_carrier.py b/unilabos/resources/bottle_carrier.py new file mode 100644 index 00000000..e680b26e --- /dev/null +++ b/unilabos/resources/bottle_carrier.py @@ -0,0 +1,72 @@ +""" +自动化液体处理工作站物料类定义 - 简化版 +Automated Liquid Handling Station Resource Classes - Simplified Version +""" + +from __future__ import annotations + +from typing import Dict, Optional + +from pylabrobot.resources.coordinate import Coordinate +from pylabrobot.resources.container import Container +from pylabrobot.resources.carrier import TubeCarrier +from pylabrobot.resources.resource_holder import ResourceHolder + + +class Bottle(Container): + """瓶子类 - 简化版,不追踪瓶盖""" + + def __init__( + self, + name: str, + diameter: float, + height: float, + max_volume: float, + barcode: Optional[str] = "", + category: str = "container", + model: Optional[str] = None, + ): + super().__init__( + name=name, + size_x=diameter, + size_y=diameter, + size_z=height, + max_volume=max_volume, + category=category, + model=model, + ) + self.diameter = diameter + self.height = height + self.barcode = barcode + + def serialize(self) -> dict: + return { + **super().serialize(), + "diameter": self.diameter, + "height": self.height, + "barcode": self.barcode, + } + + +class BottleCarrier(TubeCarrier): + """瓶载架 - 直接继承自 TubeCarrier""" + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + sites: Optional[Dict[int, ResourceHolder]] = None, + category: str = "bottle_carrier", + model: Optional[str] = None, + ): + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + sites=sites, + category=category, + model=model, + ) diff --git a/unilabos/resources/warehouse.py b/unilabos/resources/warehouse.py new file mode 100644 index 00000000..5561b94e --- /dev/null +++ b/unilabos/resources/warehouse.py @@ -0,0 +1,79 @@ +import json +from typing import Optional, List +from pylabrobot.resources import Coordinate, Resource +from pylabrobot.resources.carrier import Carrier, PlateHolder, ResourceHolder, create_homogeneous_resources +from pylabrobot.resources.deck import Deck + + +class WareHouse(Carrier[ResourceHolder]): + """4x4x1堆栈载体类 - 可容纳16个板位的载体(4层x4行x1列)""" + + def __init__( + self, + name: str, + num_items_x: int = 4, + num_items_y: int = 1, + num_items_z: int = 4, + dx: float = 137.0, + dy: float = 96.0, + dz: float = 120.0, + item_dx: float = 10.0, + item_dy: float = 10.0, + item_dz: float = 10.0, + removed_positions: Optional[List[int]] = None, + category: str = "warehouse", + model: Optional[str] = None, + ): + # 创建16个板架位 (4层 x 4位置) + locations = [] + + for layer in range(num_items_z): # 4层 + for row in range(num_items_y): # 4行 + for col in range(num_items_x): # 1列 (每层4x1=4个位置) + # 计算位置 + x = dx + col * item_dx + y = dy + (num_items_y - row - 1) * item_dy + z = dz + (num_items_z - layer - 1) * item_dz + + locations.append(Coordinate(x, y, z)) + if removed_positions: + locations = [loc for i, loc in enumerate(locations) if i not in removed_positions] + + sites = create_homogeneous_resources( + klass=ResourceHolder, + locations=[ + Coordinate(4.0, 8.5, 86.15), + Coordinate(4.0, 104.5, 86.15), + Coordinate(4.0, 200.5, 86.15), + Coordinate(4.0, 296.5, 86.15), + Coordinate(4.0, 392.5, 86.15), + ], + resource_size_x=127.0, + resource_size_y=86.0, + name_prefix=name, + ) + + super().__init__( + name=name, + size_x=dx + item_dx * num_items_x, + size_y=dy + item_dy * num_items_y, + size_z=dz + item_dz * num_items_z, + sites=sites, + category=category, + model=model, + ) + + def get_site_by_layer_position(self, row: int, col: int, layer: int) -> PlateHolder: + if not (0 <= layer < 4 and 0 <= row < 4 and 0 <= col < 1): + raise ValueError("无效的位置: layer={}, row={}, col={}".format(layer, row, col)) + + site_index = layer * 4 + row * 1 + col + return self.sites[site_index] + + def add_rack_to_position(self, row: int, col: int, layer: int, rack) -> None: + site = self.get_site_by_layer_position(row, col, layer) + site.assign_child_resource(rack) + + def get_rack_at_position(self, row: int, col: int, layer: int): + site = self.get_site_by_layer_position(row, col, layer) + return site.resource \ No newline at end of file From 63ab1af45da428c8e518a86d2465def58a79c52e Mon Sep 17 00:00:00 2001 From: Junhan Chang Date: Thu, 25 Sep 2025 08:14:48 +0800 Subject: [PATCH 04/17] refactor and add BIOYOND resources tests --- test/resources/bioyond_materials.json | 198 ++++++++++++++++ ...ttle_carrier.py => test_bottle_carrier.py} | 10 +- test/resources/test_converter_bioyond.py | 31 +++ .../resources/bioyond/bottle_carriers.yaml | 38 ++++ unilabos/resources/bioyond/bottle_carrier.py | 78 ------- unilabos/resources/bioyond/bottle_carriers.py | 213 ++++++++++++++++++ .../bioyond/{bottle.py => bottles.py} | 38 ++-- unilabos/resources/bioyond/decks.py | 48 ++++ .../bioyond/{warehouse.py => warehouses.py} | 6 +- unilabos/resources/graphio.py | 51 ++++- unilabos/resources/warehouse.py | 3 + 11 files changed, 606 insertions(+), 108 deletions(-) create mode 100644 test/resources/bioyond_materials.json rename test/resources/{bottle_carrier.py => test_bottle_carrier.py} (76%) create mode 100644 test/resources/test_converter_bioyond.py create mode 100644 unilabos/registry/resources/bioyond/bottle_carriers.yaml delete mode 100644 unilabos/resources/bioyond/bottle_carrier.py create mode 100644 unilabos/resources/bioyond/bottle_carriers.py rename unilabos/resources/bioyond/{bottle.py => bottles.py} (53%) create mode 100644 unilabos/resources/bioyond/decks.py rename unilabos/resources/bioyond/{warehouse.py => warehouses.py} (90%) diff --git a/test/resources/bioyond_materials.json b/test/resources/bioyond_materials.json new file mode 100644 index 00000000..9e6d7f6d --- /dev/null +++ b/test/resources/bioyond_materials.json @@ -0,0 +1,198 @@ +{ + "data": [ + { + "id": "3a1c67a9-aed7-b94d-9e24-bfdf10c8baa9", + "typeName": "烧杯", + "code": "0006-00160", + "barCode": "", + "name": "ODA", + "quantity": 120000.00000000000000000000000, + "lockQuantity": 695374.00000000000000000000000, + "unit": "微升", + "status": 1, + "isUse": false, + "locations": [ + { + "id": "3a14aa17-0d49-11d7-a6e1-f236b3e5e5a3", + "whid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366", + "whName": "堆栈1", + "code": "0001-0001", + "x": 1, + "y": 1, + "z": 1, + "quantity": 0 + } + ], + "detail": [] + }, + { + "id": "3a1c67a9-aed9-1ade-5fe1-cc04b24b171c", + "typeName": "烧杯", + "code": "0006-00161", + "barCode": "", + "name": "MPDA", + "quantity": 120000.00000000000000000000000, + "lockQuantity": 681618.00000000000000000000000, + "unit": "", + "status": 1, + "isUse": false, + "locations": [ + { + "id": "3a14aa17-0d49-4bc5-8836-517b75473f5f", + "whid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366", + "whName": "堆栈1", + "code": "0001-0002", + "x": 1, + "y": 2, + "z": 1, + "quantity": 0 + } + ], + "detail": [] + }, + { + "id": "3a1c67a9-aed9-2864-6783-2cee4e701ba6", + "typeName": "试剂瓶", + "code": "0004-00041", + "barCode": "", + "name": "NMP", + "quantity": 300000.00000000000000000000000, + "lockQuantity": 380000.00000000000000000000000, + "unit": "微升", + "status": 1, + "isUse": false, + "locations": [ + { + "id": "3a14aa3b-9fab-adac-7b9c-e1ee446b51d5", + "whid": "3a14aa3b-9fab-9d8e-d1a7-828f01f51f0c", + "whName": "站内试剂存放堆栈", + "code": "0003-0001", + "x": 1, + "y": 1, + "z": 1, + "quantity": 0 + } + ], + "detail": [] + }, + { + "id": "3a1c67a9-aed9-32c7-5809-3ba1b8db1aa1", + "typeName": "试剂瓶", + "code": "0004-00042", + "barCode": "", + "name": "PGME", + "quantity": 300000.00000000000000000000000, + "lockQuantity": 337892.00000000000000000000000, + "unit": "", + "status": 1, + "isUse": false, + "locations": [ + { + "id": "3a14aa3b-9fab-ca72-febc-b7c304476c78", + "whid": "3a14aa3b-9fab-9d8e-d1a7-828f01f51f0c", + "whName": "站内试剂存放堆栈", + "code": "0003-0002", + "x": 1, + "y": 2, + "z": 1, + "quantity": 0 + } + ], + "detail": [] + }, + { + "id": "3a1c68c8-0574-d748-725e-97a2e549f085", + "typeName": "样品板", + "code": "0001-00004", + "barCode": "", + "name": "0917", + "quantity": 1.0000000000000000000000000000, + "lockQuantity": 4.0000000000000000000000000000, + "unit": "块", + "status": 1, + "isUse": false, + "locations": [ + { + "id": "3a14aa17-0d49-f49c-6b66-b27f185a3b32", + "whid": "3a14aa17-0d49-dce4-486e-4b5c85c8b366", + "whName": "堆栈1", + "code": "0001-0009", + "x": 2, + "y": 1, + "z": 1, + "quantity": 0 + } + ], + "detail": [ + { + "id": "3a1c68c8-0574-69a1-9858-4637e0193451", + "detailMaterialId": "3a1c68c8-0574-3630-bd42-bbf3623c5208", + "code": null, + "name": "SIDA", + "quantity": "300000", + "lockQuantity": "4", + "unit": "微升", + "x": 1, + "y": 2, + "z": 1, + "associateId": null + }, + { + "id": "3a1c68c8-0574-8d51-3191-a31f5be421e5", + "detailMaterialId": "3a1c68c8-0574-3b20-9ad7-90755f123d53", + "code": null, + "name": "BTDA-2", + "quantity": "300000", + "lockQuantity": "4", + "unit": "微升", + "x": 2, + "y": 2, + "z": 1, + "associateId": null + }, + { + "id": "3a1c68c8-0574-da80-735b-53ae2197a360", + "detailMaterialId": "3a1c68c8-0574-f2e4-33b3-90d813567939", + "code": null, + "name": "BTDA-DD", + "quantity": "300000", + "lockQuantity": "28", + "unit": "微升", + "x": 1, + "y": 1, + "z": 1, + "associateId": null + }, + { + "id": "3a1c68c8-0574-e717-1b1b-99891f875455", + "detailMaterialId": "3a1c68c8-0574-a0ef-e636-68cdc98960e2", + "code": null, + "name": "BTDA-3", + "quantity": "300000", + "lockQuantity": "4", + "unit": "微升", + "x": 2, + "y": 3, + "z": 1, + "associateId": null + }, + { + "id": "3a1c68c8-0574-e9bd-6cca-5e261b4f89cb", + "detailMaterialId": "3a1c68c8-0574-9d11-5115-283e8e5510b1", + "code": null, + "name": "BTDA-1", + "quantity": "300000", + "lockQuantity": "4", + "unit": "微升", + "x": 2, + "y": 1, + "z": 1, + "associateId": null + } + ] + } + ], + "code": 1, + "message": "", + "timestamp": 1758560573511 +} \ No newline at end of file diff --git a/test/resources/bottle_carrier.py b/test/resources/test_bottle_carrier.py similarity index 76% rename from test/resources/bottle_carrier.py rename to test/resources/test_bottle_carrier.py index 162ead9e..22ef6f51 100644 --- a/test/resources/bottle_carrier.py +++ b/test/resources/test_bottle_carrier.py @@ -1,7 +1,7 @@ import pytest -from unilabos.resources.bioyond.bottle_carrier import BIOYOND_Electrolyte_6VialCarrier, BIOYOND_Electrolyte_1BottleCarrier -from unilabos.resources.bioyond.bottle import create_powder_bottle, create_solution_beaker, create_reagent_bottle +from unilabos.resources.bioyond.bottle_carriers import BIOYOND_Electrolyte_6VialCarrier, BIOYOND_Electrolyte_1BottleCarrier +from unilabos.resources.bioyond.bottles import BIOYOND_PolymerStation_Solid_Vial, BIOYOND_PolymerStation_Solution_Beaker, BIOYOND_PolymerStation_Reagent_Bottle @pytest.fixture @@ -17,9 +17,9 @@ def bottle_carrier() -> "BottleCarrier": print(f"1烧杯载架: {beaker_carrier.name}, 位置数: {len(beaker_carrier.sites)}") # 创建瓶子和烧杯 - powder_bottle = create_powder_bottle("powder_bottle_01") - solution_beaker = create_solution_beaker("solution_beaker_01") - reagent_bottle = create_reagent_bottle("reagent_bottle_01") + powder_bottle = BIOYOND_PolymerStation_Solid_Vial("powder_bottle_01") + solution_beaker = BIOYOND_PolymerStation_Solution_Beaker("solution_beaker_01") + reagent_bottle = BIOYOND_PolymerStation_Reagent_Bottle("reagent_bottle_01") print(f"\n创建的物料:") print(f"粉末瓶: {powder_bottle.name} - {powder_bottle.diameter}mm x {powder_bottle.height}mm, {powder_bottle.max_volume}μL") diff --git a/test/resources/test_converter_bioyond.py b/test/resources/test_converter_bioyond.py new file mode 100644 index 00000000..6c054c29 --- /dev/null +++ b/test/resources/test_converter_bioyond.py @@ -0,0 +1,31 @@ +import pytest +import json +import os + +from unilabos.resources.graphio import resource_bioyond_to_plr +from unilabos.registry.registry import lab_registry + +lab_registry.setup() + + +type_mapping = { + "烧杯": "BIOYOND_PolymerStation_1FlaskCarrier", + "试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier", + "样品板": "BIOYOND_PolymerStation_6VialCarrier", +} + +@pytest.fixture +def bioyond_materials() -> list[dict]: + print("加载 BioYond 物料数据...") + print(os.getcwd()) + with open("bioyond_materials.json", "r", encoding="utf-8") as f: + data = json.load(f)["data"] + print(f"加载了 {len(data)} 条物料数据") + return data + + +def test_bioyond_to_plr(bioyond_materials) -> list[dict]: + print("将 BioYond 物料数据转换为 PLR 格式...") + output = resource_bioyond_to_plr(bioyond_materials, type_mapping=type_mapping) + print([resource.serialize() for resource in output]) + print([resource.serialize_all_state() for resource in output]) diff --git a/unilabos/registry/resources/bioyond/bottle_carriers.yaml b/unilabos/registry/resources/bioyond/bottle_carriers.yaml new file mode 100644 index 00000000..221e6c74 --- /dev/null +++ b/unilabos/registry/resources/bioyond/bottle_carriers.yaml @@ -0,0 +1,38 @@ +BIOYOND_PolymerStation_6VialCarrier: + category: + - bottle_carriers + class: + module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_6VialCarrier + type: pylabrobot + description: BIOYOND_PolymerStation_6VialCarrier + handles: [ ] + icon: '' + init_param_schema: { } + registry_type: resource + version: 1.0.0 + +BIOYOND_PolymerStation_1BottleCarrier: + category: + - bottle_carriers + class: + module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1BottleCarrier + type: pylabrobot + description: BIOYOND_PolymerStation_1BottleCarrier + handles: [ ] + icon: '' + init_param_schema: { } + registry_type: resource + version: 1.0.0 + +BIOYOND_PolymerStation_1FlaskCarrier: + category: + - bottle_carriers + class: + module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1FlaskCarrier + type: pylabrobot + description: BIOYOND_PolymerStation_1FlaskCarrier + handles: [ ] + icon: '' + init_param_schema: { } + registry_type: resource + version: 1.0.0 \ No newline at end of file diff --git a/unilabos/resources/bioyond/bottle_carrier.py b/unilabos/resources/bioyond/bottle_carrier.py deleted file mode 100644 index e7ac488e..00000000 --- a/unilabos/resources/bioyond/bottle_carrier.py +++ /dev/null @@ -1,78 +0,0 @@ -from unilabos.resources.bottle_carrier import Bottle, BottleCarrier -from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder - - -# 命名约定:试剂瓶-Bottle,烧杯-Beaker,烧瓶-Flask,小瓶-Vial - -def BIOYOND_Electrolyte_6VialCarrier(name: str) -> BottleCarrier: - """6瓶载架 - 2x3布局""" - - # 载架尺寸 (mm) - carrier_size_x = 127.8 - carrier_size_y = 85.5 - carrier_size_z = 50.0 - - # 瓶位尺寸 - bottle_diameter = 30.0 - bottle_spacing_x = 42.0 # X方向间距 - bottle_spacing_y = 35.0 # Y方向间距 - - # 计算起始位置 (居中排列) - start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2 - start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 - - # 创建6个位置坐标 (2行 x 3列) - locations = [] - for row in range(2): - for col in range(3): - x = start_x + col * bottle_spacing_x - y = start_y + row * bottle_spacing_y - z = 5.0 # 架位底部 - locations.append(Coordinate(x, y, z)) - - return BottleCarrier( - name=name, - size_x=carrier_size_x, - size_y=carrier_size_y, - size_z=carrier_size_z, - sites=create_homogeneous_resources( - klass=ResourceHolder, - locations=locations, - resource_size_x=bottle_diameter, - resource_size_y=bottle_diameter, - name_prefix=name, - ), - model="BIOYOND_Electrolyte_6VialCarrier", - ) - - -def BIOYOND_Electrolyte_1BottleCarrier(name: str) -> BottleCarrier: - """1瓶载架 - 单个中央位置""" - - # 载架尺寸 (mm) - carrier_size_x = 127.8 - carrier_size_y = 85.5 - carrier_size_z = 100.0 - - # 烧杯尺寸 - beaker_diameter = 80.0 - - # 计算中央位置 - center_x = (carrier_size_x - beaker_diameter) / 2 - center_y = (carrier_size_y - beaker_diameter) / 2 - center_z = 5.0 - - return BottleCarrier( - name=name, - size_x=carrier_size_x, - size_y=carrier_size_y, - size_z=carrier_size_z, - sites=create_homogeneous_resources( - klass=ResourceHolder, - locations=[Coordinate(center_x, center_y, center_z)], - resource_size_x=beaker_diameter, - resource_size_y=beaker_diameter, - name_prefix=name, - ), - model="BIOYOND_Electrolyte_1BottleCarrier", - ) \ No newline at end of file diff --git a/unilabos/resources/bioyond/bottle_carriers.py b/unilabos/resources/bioyond/bottle_carriers.py new file mode 100644 index 00000000..cd84944b --- /dev/null +++ b/unilabos/resources/bioyond/bottle_carriers.py @@ -0,0 +1,213 @@ +from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder + +from unilabos.resources.bottle_carrier import Bottle, BottleCarrier +from unilabos.resources.bioyond.bottles import BIOYOND_PolymerStation_Solid_Vial, BIOYOND_PolymerStation_Solution_Beaker, BIOYOND_PolymerStation_Reagent_Bottle +# 命名约定:试剂瓶-Bottle,烧杯-Beaker,烧瓶-Flask,小瓶-Vial + + +def BIOYOND_Electrolyte_6VialCarrier(name: str) -> BottleCarrier: + """6瓶载架 - 2x3布局""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 50.0 + + # 瓶位尺寸 + bottle_diameter = 30.0 + bottle_spacing_x = 42.0 # X方向间距 + bottle_spacing_y = 35.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + # 创建6个位置坐标 (2行 x 3列) + locations = [] + for row in range(2): + for col in range(3): + x = start_x + col * bottle_spacing_x + y = start_y + row * bottle_spacing_y + z = 5.0 # 架位底部 + locations.append(Coordinate(x, y, z)) + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=create_homogeneous_resources( + klass=ResourceHolder, + locations=locations, + resource_size_x=bottle_diameter, + resource_size_y=bottle_diameter, + name_prefix=name, + ), + model="BIOYOND_Electrolyte_6VialCarrier", + ) + carrier.num_items_x = 3 + carrier.num_items_y = 2 + carrier.num_items_z = 1 + for i in range(6): + carrier[i] = BIOYOND_PolymerStation_Solid_Vial(f"{name}_vial_{i+1}") + return carrier + + +def BIOYOND_Electrolyte_1BottleCarrier(name: str) -> BottleCarrier: + """1瓶载架 - 单个中央位置""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 100.0 + + # 烧杯尺寸 + beaker_diameter = 80.0 + + # 计算中央位置 + center_x = (carrier_size_x - beaker_diameter) / 2 + center_y = (carrier_size_y - beaker_diameter) / 2 + center_z = 5.0 + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=create_homogeneous_resources( + klass=ResourceHolder, + locations=[Coordinate(center_x, center_y, center_z)], + resource_size_x=beaker_diameter, + resource_size_y=beaker_diameter, + name_prefix=name, + ), + model="BIOYOND_Electrolyte_1BottleCarrier", + ) + carrier.num_items_x = 1 + carrier.num_items_y = 1 + carrier.num_items_z = 1 + carrier[0] = BIOYOND_PolymerStation_Solution_Beaker(f"{name}_beaker_1") + return carrier + + +def BIOYOND_PolymerStation_6VialCarrier(name: str) -> BottleCarrier: + """6瓶载架 - 2x3布局""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 50.0 + + # 瓶位尺寸 + bottle_diameter = 30.0 + bottle_spacing_x = 42.0 # X方向间距 + bottle_spacing_y = 35.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + # 创建6个位置坐标 (2行 x 3列) + locations = [] + for row in range(2): + for col in range(3): + x = start_x + col * bottle_spacing_x + y = start_y + row * bottle_spacing_y + z = 5.0 # 架位底部 + locations.append(Coordinate(x, y, z)) + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=create_homogeneous_resources( + klass=ResourceHolder, + locations=locations, + resource_size_x=bottle_diameter, + resource_size_y=bottle_diameter, + name_prefix=name, + ), + model="BIOYOND_PolymerStation_6VialCarrier", + ) + carrier.num_items_x = 3 + carrier.num_items_y = 2 + carrier.num_items_z = 1 + ordering = ["A1", "A2", "A3", "B1", "B2", "B3"] # 自定义顺序 + for i in range(6): + carrier[i] = BIOYOND_PolymerStation_Solid_Vial(f"{name}_vial_{ordering[i]}") + return carrier + + +def BIOYOND_PolymerStation_1BottleCarrier(name: str) -> BottleCarrier: + """1瓶载架 - 单个中央位置""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 20.0 + + # 烧杯尺寸 + beaker_diameter = 60.0 + + # 计算中央位置 + center_x = (carrier_size_x - beaker_diameter) / 2 + center_y = (carrier_size_y - beaker_diameter) / 2 + center_z = 5.0 + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=create_homogeneous_resources( + klass=ResourceHolder, + locations=[Coordinate(center_x, center_y, center_z)], + resource_size_x=beaker_diameter, + resource_size_y=beaker_diameter, + name_prefix=name, + ), + model="BIOYOND_PolymerStation_1BottleCarrier", + ) + carrier.num_items_x = 1 + carrier.num_items_y = 1 + carrier.num_items_z = 1 + carrier[0] = BIOYOND_PolymerStation_Reagent_Bottle(f"{name}_flask_1") + return carrier + + +def BIOYOND_PolymerStation_1FlaskCarrier(name: str) -> BottleCarrier: + """1瓶载架 - 单个中央位置""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 20.0 + + # 烧杯尺寸 + beaker_diameter = 70.0 + + # 计算中央位置 + center_x = (carrier_size_x - beaker_diameter) / 2 + center_y = (carrier_size_y - beaker_diameter) / 2 + center_z = 5.0 + + carrier = BottleCarrier( + name=name, + size_x=carrier_size_x, + size_y=carrier_size_y, + size_z=carrier_size_z, + sites=create_homogeneous_resources( + klass=ResourceHolder, + locations=[Coordinate(center_x, center_y, center_z)], + resource_size_x=beaker_diameter, + resource_size_y=beaker_diameter, + name_prefix=name, + ), + model="BIOYOND_PolymerStation_1FlaskCarrier", + ) + carrier.num_items_x = 1 + carrier.num_items_y = 1 + carrier.num_items_z = 1 + carrier[0] = BIOYOND_PolymerStation_Reagent_Bottle(f"{name}_bottle_1") + return carrier diff --git a/unilabos/resources/bioyond/bottle.py b/unilabos/resources/bioyond/bottles.py similarity index 53% rename from unilabos/resources/bioyond/bottle.py rename to unilabos/resources/bioyond/bottles.py index f642ca2e..b3725de9 100644 --- a/unilabos/resources/bioyond/bottle.py +++ b/unilabos/resources/bioyond/bottles.py @@ -2,11 +2,12 @@ from unilabos.resources.bottle_carrier import Bottle, BottleCarrier # 工厂函数 -def create_powder_bottle( +def BIOYOND_PolymerStation_Solid_Vial( name: str, - diameter: float = 30.0, - height: float = 50.0, - max_volume: float = 50000.0, # 50mL + diameter: float = 20.0, + height: float = 100.0, + max_volume: float = 30000.0, # 30mL + barcode: str = None, ) -> Bottle: """创建粉末瓶""" return Bottle( @@ -14,15 +15,17 @@ def create_powder_bottle( diameter=diameter, height=height, max_volume=max_volume, - category="powder_bottle", + barcode=barcode, + model="BIOYOND_PolymerStation_Solid_Vial", ) -def create_solution_beaker( +def BIOYOND_PolymerStation_Solution_Beaker( name: str, - diameter: float = 80.0, - height: float = 100.0, - max_volume: float = 500000.0, # 500mL + diameter: float = 60.0, + height: float = 70.0, + max_volume: float = 200000.0, # 200mL + barcode: str = None, ) -> Bottle: """创建溶液烧杯""" return Bottle( @@ -30,15 +33,17 @@ def create_solution_beaker( diameter=diameter, height=height, max_volume=max_volume, - category="solution_beaker", + barcode=barcode, + model="BIOYOND_PolymerStation_Solution_Beaker", ) -def create_reagent_bottle( +def BIOYOND_PolymerStation_Reagent_Bottle( name: str, - diameter: float = 20.0, - height: float = 40.0, - max_volume: float = 15000.0, # 15mL + diameter: float = 70.0, + height: float = 120.0, + max_volume: float = 500000.0, # 500mL + barcode: str = None, ) -> Bottle: """创建试剂瓶""" return Bottle( @@ -46,5 +51,6 @@ def create_reagent_bottle( diameter=diameter, height=height, max_volume=max_volume, - category="reagent_bottle", - ) \ No newline at end of file + barcode=barcode, + model="BIOYOND_PolymerStation_Reagent_Bottle", + ) diff --git a/unilabos/resources/bioyond/decks.py b/unilabos/resources/bioyond/decks.py new file mode 100644 index 00000000..56f556ca --- /dev/null +++ b/unilabos/resources/bioyond/decks.py @@ -0,0 +1,48 @@ +from pylabrobot.resources import Deck, Coordinate + +from unilabos.resources.bioyond.warehouses import bioyond_warehouse_1x4x4, bioyond_warehouse_1x4x2, bioyond_warehouse_liquid_and_lid_handling + + +class BIOYOND_PolymerReactionStation_Deck(Deck): + def __init__(self, name: str = "PolymerReactionStation_Deck") -> None: + super().__init__(name=name, size_x=2700.0, size_y=1080.0, size_z=1500.0) + + def setup(self) -> None: + # 添加仓库 + self.warehouses = { + "io_warehouse_left": bioyond_warehouse_1x4x4("io_warehouse_left"), + "io_warehouse_right": bioyond_warehouse_1x4x4("io_warehouse_right"), + "liquid_and_lid_handling": bioyond_warehouse_liquid_and_lid_handling("liquid_and_lid_handling"), + } + self.warehouse_locations = { + "io_warehouse_left": Coordinate(0.0, 650.0, 0.0), + "io_warehouse_right": Coordinate(2550.0, 650.0, 0.0), + "liquid_and_lid_handling": Coordinate(800.0, 475.0, 0.0), + } + + for warehouse_name, warehouse in self.warehouses.items(): + self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name]) + + +class BIOYOND_PolymerPreparationStation_Deck(Deck): + def __init__(self, name: str = "PolymerPreparationStation_Deck") -> None: + super().__init__(name=name, size_x=2700.0, size_y=1080.0, size_z=1500.0) + self.warehouses = {} + + def setup(self) -> None: + # 添加仓库 + self.warehouses = { + "io_warehouse_left": bioyond_warehouse_1x4x4("io_warehouse_left"), + "io_warehouse_right": bioyond_warehouse_1x4x4("io_warehouse_right"), + "solutions": bioyond_warehouse_1x4x2("warehouse_solutions"), + "liquid_and_lid_handling": bioyond_warehouse_liquid_and_lid_handling("warehouse_liquid_and_lid_handling"), + } + self.warehouse_locations = { + "io_warehouse_left": Coordinate(0.0, 650.0, 0.0), + "io_warehouse_right": Coordinate(2550.0, 650.0, 0.0), + "solutions": Coordinate(1915.0, 900.0, 0.0), + "liquid_and_lid_handling": Coordinate(1330.0, 490.0, 0.0), + } + + for warehouse_name, warehouse in self.warehouses.items(): + self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name]) diff --git a/unilabos/resources/bioyond/warehouse.py b/unilabos/resources/bioyond/warehouses.py similarity index 90% rename from unilabos/resources/bioyond/warehouse.py rename to unilabos/resources/bioyond/warehouses.py index c468f875..d075e559 100644 --- a/unilabos/resources/bioyond/warehouse.py +++ b/unilabos/resources/bioyond/warehouses.py @@ -18,12 +18,12 @@ def bioyond_warehouse_1x4x4(name: str) -> WareHouse: ) -def bioyond_warehouse_1x3x2(name: str) -> WareHouse: - """创建BioYond 3x1x2仓库""" +def bioyond_warehouse_1x4x2(name: str) -> WareHouse: + """创建BioYond 4x1x2仓库""" return WareHouse( name=name, num_items_x=1, - num_items_y=3, + num_items_y=4, num_items_z=2, dx=137.0, dy=96.0, diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index 093aa7d5..c56abc56 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -480,7 +480,43 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w return r -def initialize_resource(resource_config: dict) -> list[dict]: +def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict = {}, location_id_mapping: dict = None) -> list[dict]: + """ + 将 bioyond 物料格式转换为 ulab 物料格式 + + Args: + bioyond_materials: bioyond 系统的物料查询结果列表 + type_mapping: 物料类型映射字典,格式 {bioyond_type: plr_class_name} + location_id_mapping: 库位 ID 到名称的映射字典,格式 {location_id: location_name} + + Returns: + pylabrobot 格式的物料列表 + """ + plr_materials = [] + + for material in bioyond_materials: + className = type_mapping.get(material.get("typeName"), "RegularContainer") if type_mapping else "RegularContainer" + + plr_material: ResourcePLR = initialize_resource({"name": material["name"], "class": className}, resource_type=ResourcePLR) + plr_material.code = material.get("code", "") and material.get("barCode", "") or "" + + # 处理子物料(detail) + if material.get("detail") and len(material["detail"]) > 0: + child_ids = [] + for detail in material["detail"]: + number = (detail.get("z", 0) - 1) * plr_material.num_items_x * plr_material.num_items_y + \ + (detail.get("x", 0) - 1) * plr_material.num_items_x + \ + (detail.get("y", 0) - 1) + bottle = plr_material[number].resource + bottle.code = detail.get("code", "") + bottle.tracker.liquids = [(detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0)] + + plr_materials.append(plr_material) + + return plr_materials + + +def initialize_resource(resource_config: dict, resource_type: Any = None) -> Union[list[dict], ResourcePLR]: """Initializes a resource based on its configuration. If the config is detailed, then do nothing; @@ -512,11 +548,14 @@ def initialize_resource(resource_config: dict) -> list[dict]: if resource_class_config["type"] == "pylabrobot": resource_plr = RESOURCE(name=resource_config["name"]) - r = resource_plr_to_ulab(resource_plr=resource_plr, parent_name=resource_config.get("parent", None)) - # r = resource_plr_to_ulab(resource_plr=resource_plr) - if resource_config.get("position") is not None: - r["position"] = resource_config["position"] - r = tree_to_list([r]) + if resource_type != ResourcePLR: + r = resource_plr_to_ulab(resource_plr=resource_plr, parent_name=resource_config.get("parent", None)) + # r = resource_plr_to_ulab(resource_plr=resource_plr) + if resource_config.get("position") is not None: + r["position"] = resource_config["position"] + r = tree_to_list([r]) + else: + r = resource_plr elif resource_class_config["type"] == "unilabos": res_instance: RegularContainer = RESOURCE(id=resource_config["name"]) res_instance.ulr_resource = convert_to_ros_msg(Resource, {k:v for k,v in resource_config.items() if k != "class"}) diff --git a/unilabos/resources/warehouse.py b/unilabos/resources/warehouse.py index 5561b94e..11b945ed 100644 --- a/unilabos/resources/warehouse.py +++ b/unilabos/resources/warehouse.py @@ -24,6 +24,9 @@ class WareHouse(Carrier[ResourceHolder]): category: str = "warehouse", model: Optional[str] = None, ): + self.num_items_x = num_items_x + self.num_items_y = num_items_y + self.num_items_z = num_items_z # 创建16个板架位 (4层 x 4位置) locations = [] From 5c47cd0c8a73ce840490246b05f58f06fa348fe4 Mon Sep 17 00:00:00 2001 From: Junhan Chang Date: Thu, 25 Sep 2025 08:41:41 +0800 Subject: [PATCH 05/17] add BIOYOND deck assignment and pass all tests --- test/resources/test_bottle_carrier.py | 7 ++--- test/resources/test_converter_bioyond.py | 6 +++- unilabos/resources/bioyond/decks.py | 14 +++++---- unilabos/resources/bioyond/warehouses.py | 36 ++++++++++++------------ unilabos/resources/graphio.py | 13 ++++++++- 5 files changed, 46 insertions(+), 30 deletions(-) diff --git a/test/resources/test_bottle_carrier.py b/test/resources/test_bottle_carrier.py index 22ef6f51..c981eeeb 100644 --- a/test/resources/test_bottle_carrier.py +++ b/test/resources/test_bottle_carrier.py @@ -4,8 +4,7 @@ from unilabos.resources.bioyond.bottle_carriers import BIOYOND_Electrolyte_6Vial from unilabos.resources.bioyond.bottles import BIOYOND_PolymerStation_Solid_Vial, BIOYOND_PolymerStation_Solution_Beaker, BIOYOND_PolymerStation_Reagent_Bottle -@pytest.fixture -def bottle_carrier() -> "BottleCarrier": +def test_bottle_carrier() -> "BottleCarrier": print("创建载架...") # 创建6瓶载架 @@ -30,10 +29,10 @@ def bottle_carrier() -> "BottleCarrier": print(f"\n测试放置容器...") # 通过载架的索引操作来放置容器 - bottle_carrier[0] = powder_bottle # 放置粉末瓶到第一个位置 + # bottle_carrier[0] = powder_bottle # 放置粉末瓶到第一个位置 print(f"粉末瓶已放置到6瓶载架的位置 0") - beaker_carrier[0] = solution_beaker # 放置烧杯到第一个位置 + # beaker_carrier[0] = solution_beaker # 放置烧杯到第一个位置 print(f"溶液烧杯已放置到1烧杯载架的位置 0") # 验证放置结果 diff --git a/test/resources/test_converter_bioyond.py b/test/resources/test_converter_bioyond.py index 6c054c29..1044a8d0 100644 --- a/test/resources/test_converter_bioyond.py +++ b/test/resources/test_converter_bioyond.py @@ -5,6 +5,8 @@ import os from unilabos.resources.graphio import resource_bioyond_to_plr from unilabos.registry.registry import lab_registry +from unilabos.resources.bioyond.decks import BIOYOND_PolymerReactionStation_Deck + lab_registry.setup() @@ -25,7 +27,9 @@ def bioyond_materials() -> list[dict]: def test_bioyond_to_plr(bioyond_materials) -> list[dict]: + deck = BIOYOND_PolymerReactionStation_Deck("test_deck") print("将 BioYond 物料数据转换为 PLR 格式...") - output = resource_bioyond_to_plr(bioyond_materials, type_mapping=type_mapping) + output = resource_bioyond_to_plr(bioyond_materials, type_mapping=type_mapping, deck=deck) + print(deck.summary()) print([resource.serialize() for resource in output]) print([resource.serialize_all_state() for resource in output]) diff --git a/unilabos/resources/bioyond/decks.py b/unilabos/resources/bioyond/decks.py index 56f556ca..d9fce97f 100644 --- a/unilabos/resources/bioyond/decks.py +++ b/unilabos/resources/bioyond/decks.py @@ -6,19 +6,21 @@ from unilabos.resources.bioyond.warehouses import bioyond_warehouse_1x4x4, bioyo class BIOYOND_PolymerReactionStation_Deck(Deck): def __init__(self, name: str = "PolymerReactionStation_Deck") -> None: super().__init__(name=name, size_x=2700.0, size_y=1080.0, size_z=1500.0) + self.setup() def setup(self) -> None: # 添加仓库 self.warehouses = { - "io_warehouse_left": bioyond_warehouse_1x4x4("io_warehouse_left"), - "io_warehouse_right": bioyond_warehouse_1x4x4("io_warehouse_right"), - "liquid_and_lid_handling": bioyond_warehouse_liquid_and_lid_handling("liquid_and_lid_handling"), + "堆栈1": bioyond_warehouse_1x4x4("堆栈1"), + "堆栈2": bioyond_warehouse_1x4x4("堆栈2"), + "站内试剂存放堆栈": bioyond_warehouse_liquid_and_lid_handling("站内试剂存放堆栈"), } self.warehouse_locations = { - "io_warehouse_left": Coordinate(0.0, 650.0, 0.0), - "io_warehouse_right": Coordinate(2550.0, 650.0, 0.0), - "liquid_and_lid_handling": Coordinate(800.0, 475.0, 0.0), + "堆栈1": Coordinate(0.0, 650.0, 0.0), + "堆栈2": Coordinate(2550.0, 650.0, 0.0), + "站内试剂存放堆栈": Coordinate(800.0, 475.0, 0.0), } + self.warehouses["站内试剂存放堆栈"].rotation = 90.0 for warehouse_name, warehouse in self.warehouses.items(): self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name]) diff --git a/unilabos/resources/bioyond/warehouses.py b/unilabos/resources/bioyond/warehouses.py index d075e559..9819a657 100644 --- a/unilabos/resources/bioyond/warehouses.py +++ b/unilabos/resources/bioyond/warehouses.py @@ -8,12 +8,12 @@ def bioyond_warehouse_1x4x4(name: str) -> WareHouse: num_items_x=1, num_items_y=4, num_items_z=4, - dx=137.0, - dy=96.0, - dz=120.0, - item_dx=10.0, - item_dy=10.0, - item_dz=10.0, + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=96.0, + item_dz=120.0, category="warehouse", ) @@ -25,12 +25,12 @@ def bioyond_warehouse_1x4x2(name: str) -> WareHouse: num_items_x=1, num_items_y=4, num_items_z=2, - dx=137.0, - dy=96.0, - dz=120.0, - item_dx=10.0, - item_dy=10.0, - item_dz=10.0, + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=96.0, + item_dz=120.0, category="warehouse", removed_positions=None ) @@ -43,12 +43,12 @@ def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse: num_items_x=2, num_items_y=5, num_items_z=1, - dx=137.0, - dy=96.0, - dz=120.0, - item_dx=10.0, - item_dy=10.0, - item_dz=10.0, + dx=10.0, + dy=10.0, + dz=10.0, + item_dx=137.0, + item_dy=96.0, + item_dz=120.0, category="warehouse", removed_positions=None ) \ No newline at end of file diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index c56abc56..4bc611a2 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -480,7 +480,7 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w return r -def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict = {}, location_id_mapping: dict = None) -> list[dict]: +def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict = {}, deck: Any = None) -> list[dict]: """ 将 bioyond 物料格式转换为 ulab 物料格式 @@ -513,6 +513,17 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict = plr_materials.append(plr_material) + if deck and hasattr(deck, "warehouses"): + for loc in material.get("locations", []): + if hasattr(deck, "warehouses") and loc.get("whName") in deck.warehouses: + warehouse = deck.warehouses[loc["whName"]] + idx = (loc.get("y", 0) - 1) * warehouse.num_items_x * warehouse.num_items_y + \ + (loc.get("x", 0) - 1) * warehouse.num_items_x + \ + (loc.get("z", 0) - 1) + if 0 <= idx < warehouse.num_items_x * warehouse.num_items_y * warehouse.num_items_z: + if warehouse[idx].resource is None: + warehouse[idx] = plr_material + return plr_materials From c3b9583eac5ebfc009344c0c1a73b479ebc27af5 Mon Sep 17 00:00:00 2001 From: Junhan Chang Date: Thu, 25 Sep 2025 15:27:05 +0800 Subject: [PATCH 06/17] fix: update resource with correct structure; remove deprecated liquid_handler set_group action --- unilabos/app/web/client.py | 2 +- unilabos/registry/devices/liquid_handler.yaml | 195 ------------------ unilabos_msgs/CMakeLists.txt | 2 - 3 files changed, 1 insertion(+), 198 deletions(-) diff --git a/unilabos/app/web/client.py b/unilabos/app/web/client.py index 6eedfdd8..61a98511 100644 --- a/unilabos/app/web/client.py +++ b/unilabos/app/web/client.py @@ -132,7 +132,7 @@ class HTTPClient: """ response = requests.put( f"{self.remote_addr}/lab/material", - json=resources, + json={"nodes": resources}, headers={"Authorization": f"Lab {self.auth}"}, timeout=100, ) diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml index 50d24152..3d9b75f9 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -7304,151 +7304,6 @@ liquid_handler.prcxi: title: LiquidHandlerRemove type: object type: LiquidHandlerRemove - set_group: - feedback: {} - goal: - group_name: group_name - volumes: volumes - wells: wells - goal_default: - group_name: '' - volumes: - - 0.0 - wells: - - category: '' - children: [] - config: '' - data: '' - id: '' - name: '' - parent: '' - pose: - orientation: - w: 1.0 - x: 0.0 - y: 0.0 - z: 0.0 - position: - x: 0.0 - y: 0.0 - z: 0.0 - sample_id: '' - type: '' - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: LiquidHandlerSetGroup_Feedback - type: object - goal: - properties: - group_name: - type: string - volumes: - items: - type: number - type: array - wells: - items: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: wells - type: object - type: array - required: - - group_name - - wells - - volumes - title: LiquidHandlerSetGroup_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: LiquidHandlerSetGroup_Result - type: object - required: - - goal - title: LiquidHandlerSetGroup - type: object - type: LiquidHandlerSetGroup set_liquid: feedback: {} goal: @@ -7824,56 +7679,6 @@ liquid_handler.prcxi: title: Transfer type: object type: Transfer - transfer_group: - feedback: {} - goal: - source_group_name: source_group_name - target_group_name: target_group_name - unit_volume: unit_volume - goal_default: - source_group_name: '' - target_group_name: '' - unit_volume: 0.0 - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: LiquidHandlerTransferGroup_Feedback - type: object - goal: - properties: - source_group_name: - type: string - target_group_name: - type: string - unit_volume: - type: number - required: - - source_group_name - - target_group_name - - unit_volume - title: LiquidHandlerTransferGroup_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: LiquidHandlerTransferGroup_Result - type: object - required: - - goal - title: LiquidHandlerTransferGroup - type: object - type: LiquidHandlerTransferGroup transfer_liquid: feedback: {} goal: diff --git a/unilabos_msgs/CMakeLists.txt b/unilabos_msgs/CMakeLists.txt index c67bdf47..0725514f 100644 --- a/unilabos_msgs/CMakeLists.txt +++ b/unilabos_msgs/CMakeLists.txt @@ -68,12 +68,10 @@ set(action_files "action/LiquidHandlerSetTipRack.action" "action/LiquidHandlerStamp.action" "action/LiquidHandlerTransfer.action" - "action/LiquidHandlerSetGroup.action" "action/LiquidHandlerTransferBiomek.action" "action/LiquidHandlerIncubateBiomek.action" "action/LiquidHandlerMoveBiomek.action" "action/LiquidHandlerOscillateBiomek.action" - "action/LiquidHandlerTransferGroup.action" "action/LiquidHandlerAdd.action" "action/LiquidHandlerMix.action" "action/LiquidHandlerMoveTo.action" From ad2e1432c68d38985de1af97877eb49490ac8d53 Mon Sep 17 00:00:00 2001 From: Xie Qiming <97236197+Andy6M@users.noreply.github.com> Date: Thu, 25 Sep 2025 18:53:04 +0800 Subject: [PATCH 07/17] =?UTF-8?q?feat:=20=E5=B0=86=E6=96=B0=E5=A8=81?= =?UTF-8?q?=E7=94=B5=E6=B1=A0=E6=B5=8B=E8=AF=95=E7=B3=BB=E7=BB=9F=E9=A9=B1?= =?UTF-8?q?=E5=8A=A8=E4=B8=8E=E9=85=8D=E7=BD=AE=E6=96=87=E4=BB=B6=E5=B9=B6?= =?UTF-8?q?=E5=85=A5=20workstation=5Fdev=5FYB2=20(#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 新威电池测试系统驱动与注册文件 * feat: bring neware driver & battery.json into workstation_dev_YB2 --- unilabos/devices/battery/battery.json | 29 + .../battery/neware_battery_test_system.py | 1042 +++++++++++++++++ .../devices/neware_battery_test_system.yaml | 344 ++++++ 3 files changed, 1415 insertions(+) create mode 100644 unilabos/devices/battery/battery.json create mode 100644 unilabos/devices/battery/neware_battery_test_system.py create mode 100644 unilabos/registry/devices/neware_battery_test_system.yaml diff --git a/unilabos/devices/battery/battery.json b/unilabos/devices/battery/battery.json new file mode 100644 index 00000000..105ba5bd --- /dev/null +++ b/unilabos/devices/battery/battery.json @@ -0,0 +1,29 @@ +{ + "nodes": [ + { + "id": "NEWARE_BATTERY_TEST_SYSTEM", + "name": "Neware Battery Test System", + "parent": null, + "type": "device", + "class": "neware_battery_test_system", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "ip": "127.0.0.1", + "port": 502, + "machine_id": 1, + "devtype": "27", + "timeout": 20, + "size_x": 500.0, + "size_y": 500.0, + "size_z": 2000.0 + }, + "data": {}, + "children": [] + } + ], + "links": [] +} \ No newline at end of file diff --git a/unilabos/devices/battery/neware_battery_test_system.py b/unilabos/devices/battery/neware_battery_test_system.py new file mode 100644 index 00000000..317e8fe5 --- /dev/null +++ b/unilabos/devices/battery/neware_battery_test_system.py @@ -0,0 +1,1042 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +新威电池测试系统设备类 +- 提供TCP通信接口查询电池通道状态 +- 支持720个通道(devid 1-7, 8, 86) +- 兼容BTSAPI getchlstatus协议 + +设备特点: +- TCP连接: 默认127.0.0.1:502 +- 通道映射: devid->subdevid->chlid 三级结构 +- 状态类型: working/stop/finish/protect/pause/false/unknown +""" + +import socket +import xml.etree.ElementTree as ET +import json +import time +from dataclasses import dataclass +from typing import Any, Dict, List, Optional, TypedDict + +from pylabrobot.resources import ResourceHolder, Coordinate, create_ordered_items_2d, Deck, Plate + +from unilabos.ros.nodes.base_device_node import ROS2DeviceNode +from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode + + +# ======================== +# 内部数据类和结构 +# ======================== + +@dataclass(frozen=True) +class ChannelKey: + devid: int + subdevid: int + chlid: int + + +@dataclass +class ChannelStatus: + state: str # working/stop/finish/protect/pause/false/unknown + color: str # 状态对应颜色 + current_A: float # 电流 (A) + voltage_V: float # 电压 (V) + totaltime_s: float # 总时间 (s) + + +class BatteryTestPositionState(TypedDict): + voltage: float # 电压 (V) + current: float # 电流 (A) + time: float # 时间 (s) - 使用totaltime + capacity: float # 容量 (Ah) + energy: float # 能量 (Wh) + + status: str # 通道状态 + color: str # 状态对应颜色 + + # 额外的inquire协议字段 + relativetime: float # 相对时间 (s) + open_or_close: int # 0=关闭, 1=打开 + step_type: str # 步骤类型 + cycle_id: int # 循环ID + step_id: int # 步骤ID + log_code: str # 日志代码 + + +class BatteryTestPosition(ResourceHolder): + def __init__( + self, + name, + size_x=60, + size_y=60, + size_z=60, + rotation=None, + category="resource_holder", + model=None, + child_location: Coordinate = Coordinate.zero(), + ): + super().__init__(name, size_x, size_y, size_z, rotation, category, model, child_location=child_location) + self._unilabos_state: Dict[str, Any] = {} + + def load_state(self, state: Dict[str, Any]) -> None: + """格式不变""" + super().load_state(state) + self._unilabos_state = state + + def serialize_state(self) -> Dict[str, Dict[str, Any]]: + """格式不变""" + data = super().serialize_state() + data.update(self._unilabos_state) + return data + + +class NewareBatteryTestSystem: + """ + 新威电池测试系统设备类 + + 提供电池测试通道状态查询、控制等功能。 + 支持720个通道的状态监控和数据导出。 + 包含完整的物料管理系统,支持2盘电池的状态映射。 + + Attributes: + ip (str): TCP服务器IP地址,默认127.0.0.1 + port (int): TCP端口,默认502 + devtype (str): 设备类型,默认"27" + timeout (int): 通信超时时间(秒),默认20 + """ + + # ======================== + # 基本通信与协议参数 + # ======================== + BTS_IP = "127.0.0.1" + BTS_PORT = 502 + DEVTYPE = "27" + TIMEOUT = 20 # 秒 + REQ_END = b"#\r\n" # 常见实现以 "#\\r\\n" 作为报文结束 + + # ======================== + # 状态与颜色映射(前端可直接使用) + # ======================== + STATUS_SET = {"working", "stop", "finish", "protect", "pause", "false"} + STATUS_COLOR = { + "working": "#22c55e", # 绿 + "stop": "#6b7280", # 灰 + "finish": "#3b82f6", # 蓝 + "protect": "#ef4444", # 红 + "pause": "#f59e0b", # 橙 + "false": "#9ca3af", # 不存在/无效 + "unknown": "#a855f7", # 未知 + } + + # 字母常量 + ascii_lowercase = 'abcdefghijklmnopqrstuvwxyz' + ascii_uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + LETTERS = ascii_uppercase + ascii_lowercase + + def __init__(self, + ip: str = None, + port: int = None, + machine_id: int = 1, + devtype: str = None, + timeout: int = None, + + size_x: float = 500.0, + size_y: float = 500.0, + size_z: float = 2000.0, + ): + """ + 初始化新威电池测试系统 + + Args: + ip: TCP服务器IP地址 + port: TCP端口 + devtype: 设备类型标识 + timeout: 通信超时时间(秒) + machine_id: 机器ID + size_x, size_y, size_z: 设备物理尺寸 + """ + self.ip = ip or self.BTS_IP + self.port = port or self.BTS_PORT + self.machine_id = machine_id + self.devtype = devtype or self.DEVTYPE + self.timeout = timeout or self.TIMEOUT + self._last_status_update = None + self._cached_status = {} + self._ros_node: Optional[ROS2WorkstationNode] = None # ROS节点引用,由框架设置 + + + def post_init(self, ros_node): + """ + ROS节点初始化后的回调方法,用于建立设备连接 + + Args: + ros_node: ROS节点实例 + """ + self._ros_node = ros_node + # 创建2盘电池的物料管理系统 + self._setup_material_management() + # 初始化通道映射 + self._channels = self._build_channel_map() + try: + # 测试设备连接 + if self.test_connection(): + ros_node.lab_logger().info(f"新威电池测试系统连接成功: {self.ip}:{self.port}") + else: + ros_node.lab_logger().warning(f"新威电池测试系统连接失败: {self.ip}:{self.port}") + except Exception as e: + ros_node.lab_logger().error(f"新威电池测试系统初始化失败: {e}") + # 不抛出异常,允许节点继续运行,后续可以重试连接 + + def _setup_material_management(self): + """设置物料管理系统""" + # 第1盘:5行8列网格 (A1-E8) - 5行对应subdevid 1-5,8列对应chlid 1-8 + # 先给物料设置一个最大的Deck + deck_main = Deck("ADeckName", 200, 200, 200) + + plate1_resources: Dict[str, BatteryTestPosition] = create_ordered_items_2d( + BatteryTestPosition, + num_items_x=8, # 8列(对应chlid 1-8) + num_items_y=5, # 5行(对应subdevid 1-5,即A-E) + dx=10, + dy=10, + dz=0, + item_dx=45, + item_dy=45 + ) + plate1 = Plate("P1", 400, 300, 50, ordered_items=plate1_resources) + deck_main.assign_child_resource(plate1, location=Coordinate(0, 0, 0)) + + # 只有在真实ROS环境下才调用update_resource + if hasattr(self._ros_node, 'update_resource') and callable(getattr(self._ros_node, 'update_resource')): + try: + ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ + "resources": [deck_main] + }) + except Exception as e: + if hasattr(self._ros_node, 'lab_logger'): + self._ros_node.lab_logger().warning(f"更新资源失败: {e}") + # 在非ROS环境下忽略此错误 + + # 为第1盘资源添加P1_前缀 + self.station_resources_plate1 = {} + for name, resource in plate1_resources.items(): + new_name = f"P1_{name}" + self.station_resources_plate1[new_name] = resource + + # 第2盘:5行8列网格 (A1-E8),在Z轴上偏移 - 5行对应subdevid 6-10,8列对应chlid 1-8 + plate2_resources = create_ordered_items_2d( + BatteryTestPosition, + num_items_x=8, # 8列(对应chlid 1-8) + num_items_y=5, # 5行(对应subdevid 6-10,即A-E) + dx=10, + dy=10, + dz=100, # Z轴偏移100mm + item_dx=65, + item_dy=65 + ) + + # 为第2盘资源添加P2_前缀 + self.station_resources_plate2 = {} + for name, resource in plate2_resources.items(): + new_name = f"P2_{name}" + self.station_resources_plate2[new_name] = resource + + # 合并两盘资源为统一的station_resources + self.station_resources = {} + self.station_resources.update(self.station_resources_plate1) + self.station_resources.update(self.station_resources_plate2) + + # ======================== + # 核心属性(Uni-Lab标准) + # ======================== + + @property + def status(self) -> str: + """设备状态属性 - 会被自动识别并定时广播""" + try: + if self.test_connection(): + return "Connected" + else: + return "Disconnected" + except: + return "Error" + + @property + def channel_status(self) -> Dict[int, Dict]: + """ + 获取所有通道状态(按设备ID分组) + + 这个属性会执行实际的TCP查询并返回格式化的状态数据。 + 结果按设备ID分组,包含统计信息和详细状态。 + + Returns: + Dict[int, Dict]: 按设备ID分组的通道状态统计 + """ + status_map = self._query_all_channels() + status_processed = {} if not status_map else self._group_by_devid(status_map) + + # 修复数据过滤逻辑:如果machine_id对应的数据不存在,尝试使用第一个可用的设备数据 + status_current_machine = status_processed.get(self.machine_id, {}) + + if not status_current_machine and status_processed: + # 如果machine_id没有匹配到数据,使用第一个可用的设备数据 + first_devid = next(iter(status_processed.keys())) + status_current_machine = status_processed[first_devid] + if self._ros_node: + self._ros_node.lab_logger().warning( + f"machine_id {self.machine_id} 没有匹配到数据,使用设备ID {first_devid} 的数据" + ) + + # 确保有默认的数据结构 + if not status_current_machine: + status_current_machine = { + "stats": {s: 0 for s in self.STATUS_SET | {"unknown"}}, + "subunits": {} + } + + # 确保subunits存在 + subunits = status_current_machine.get("subunits", {}) + + # 处理2盘电池的状态映射 + self._update_plate_resources(subunits) + + return status_current_machine + + def _update_plate_resources(self, subunits: Dict): + """更新两盘电池资源的状态""" + # 第1盘:subdevid 1-5 映射到 P1_A1-P1_E8 (5行8列) + for subdev_id in range(1, 6): # subdevid 1-5 + status_row = subunits.get(subdev_id, {}) + + for chl_id in range(1, 9): # chlid 1-8 + try: + # 计算在5×8网格中的位置 + row_idx = (subdev_id - 1) # 0-4 (对应A-E) + col_idx = (chl_id - 1) # 0-7 (对应1-8) + resource_name = f"P1_{self.LETTERS[row_idx]}{col_idx + 1}" + + r = self.station_resources.get(resource_name) + if r: + status_channel = status_row.get(chl_id, {}) + channel_state = { + "status": status_channel.get("state", "unknown"), + "color": status_channel.get("color", self.STATUS_COLOR["unknown"]), + "voltage": status_channel.get("voltage_V", 0.0), + "current": status_channel.get("current_A", 0.0), + "time": status_channel.get("totaltime_s", 0.0), + } + r.load_state(channel_state) + except (KeyError, IndexError): + continue + + # 第2盘:subdevid 6-10 映射到 P2_A1-P2_E8 (5行8列) + for subdev_id in range(6, 11): # subdevid 6-10 + status_row = subunits.get(subdev_id, {}) + + for chl_id in range(1, 9): # chlid 1-8 + try: + # 计算在5×8网格中的位置 + row_idx = (subdev_id - 6) # 0-4 (subdevid 6->0, 7->1, ..., 10->4) (对应A-E) + col_idx = (chl_id - 1) # 0-7 (对应1-8) + resource_name = f"P2_{self.LETTERS[row_idx]}{col_idx + 1}" + + r = self.station_resources.get(resource_name) + if r: + status_channel = status_row.get(chl_id, {}) + channel_state = { + "status": status_channel.get("state", "unknown"), + "color": status_channel.get("color", self.STATUS_COLOR["unknown"]), + "voltage": status_channel.get("voltage_V", 0.0), + "current": status_channel.get("current_A", 0.0), + "time": status_channel.get("totaltime_s", 0.0), + } + r.load_state(channel_state) + except (KeyError, IndexError): + continue + + @property + def connection_info(self) -> Dict[str, str]: + """获取连接信息""" + return { + "ip": self.ip, + "port": str(self.port), + "devtype": self.devtype, + "timeout": f"{self.timeout}s" + } + + @property + def total_channels(self) -> int: + """获取总通道数""" + return len(self._channels) + + # ======================== + # 设备动作方法(Uni-Lab标准) + # ======================== + + def export_status_json(self, filepath: str = "bts_status.json") -> dict: + """ + 导出当前状态到JSON文件(ROS2动作) + + Args: + filepath: 输出文件路径 + + Returns: + dict: ROS2动作结果格式 {"return_info": str, "success": bool} + """ + try: + grouped_status = self.channel_status + payload = { + "timestamp": time.time(), + "device_info": { + "ip": self.ip, + "port": self.port, + "devtype": self.devtype, + "total_channels": self.total_channels + }, + "data": grouped_status, + "color_mapping": self.STATUS_COLOR + } + + with open(filepath, "w", encoding="utf-8") as f: + json.dump(payload, f, ensure_ascii=False, indent=2) + + success_msg = f"状态数据已成功导出到: {filepath}" + if self._ros_node: + self._ros_node.lab_logger().info(success_msg) + return {"return_info": success_msg, "success": True} + + except Exception as e: + error_msg = f"导出JSON失败: {str(e)}" + if self._ros_node: + self._ros_node.lab_logger().error(error_msg) + return {"return_info": error_msg, "success": False} + + @property + def plate_status(self) -> Dict[str, Any]: + """ + 获取所有盘的状态信息(属性) + + Returns: + 包含所有盘状态信息的字典 + """ + try: + # 确保先更新所有资源的状态数据 + _ = self.channel_status # 这会触发状态更新并调用load_state + + # 手动计算两盘的状态,避免调用需要参数的get_plate_status方法 + plate1_stats = {s: 0 for s in self.STATUS_SET | {"unknown"}} + plate1_active = [] + + for name, resource in self.station_resources_plate1.items(): + state = getattr(resource, '_unilabos_state', {}) + status = state.get('status', 'unknown') + plate1_stats[status] += 1 + + if status != 'unknown': + plate1_active.append({ + 'name': name, + 'status': status, + 'color': state.get('color', self.STATUS_COLOR['unknown']), + 'voltage': state.get('voltage', 0.0), + 'current': state.get('current', 0.0), + }) + + plate2_stats = {s: 0 for s in self.STATUS_SET | {"unknown"}} + plate2_active = [] + + for name, resource in self.station_resources_plate2.items(): + state = getattr(resource, '_unilabos_state', {}) + status = state.get('status', 'unknown') + plate2_stats[status] += 1 + + if status != 'unknown': + plate2_active.append({ + 'name': name, + 'status': status, + 'color': state.get('color', self.STATUS_COLOR['unknown']), + 'voltage': state.get('voltage', 0.0), + 'current': state.get('current', 0.0), + }) + + return { + "plate1": { + 'plate_num': 1, + 'stats': plate1_stats, + 'total_positions': len(self.station_resources_plate1), + 'active_positions': len(plate1_active), + 'resources': plate1_active + }, + "plate2": { + 'plate_num': 2, + 'stats': plate2_stats, + 'total_positions': len(self.station_resources_plate2), + 'active_positions': len(plate2_active), + 'resources': plate2_active + }, + "total_plates": 2 + } + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"获取盘状态失败: {e}") + return { + "plate1": {"error": str(e)}, + "plate2": {"error": str(e)}, + "total_plates": 2 + } + + + + + + # ======================== + # 辅助方法 + # ======================== + + def test_connection(self) -> bool: + """ + 测试TCP连接是否正常 + + Returns: + bool: 连接是否成功 + """ + try: + with socket.create_connection((self.ip, self.port), timeout=5) as sock: + return True + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().debug(f"连接测试失败: {e}") + return False + + def print_status_summary(self) -> None: + """ + 打印通道状态摘要信息(支持2盘电池) + """ + try: + status_data = self.channel_status + if not status_data: + print(" 未获取到状态数据") + return + + print(f" 状态统计:") + total_channels = 0 + + # 从channel_status获取stats字段 + stats = status_data.get("stats", {}) + for state, count in stats.items(): + if isinstance(count, int) and count > 0: + color = self.STATUS_COLOR.get(state, "#000000") + print(f" {state}: {count} 个通道 ({color})") + total_channels += count + + print(f" 总计: {total_channels} 个通道") + print(f" 第1盘资源数: {len(self.station_resources_plate1)}") + print(f" 第2盘资源数: {len(self.station_resources_plate2)}") + print(f" 总资源数: {len(self.station_resources)}") + + except Exception as e: + print(f" 获取状态失败: {e}") + + def get_device_summary(self) -> dict: + """ + 获取设备级别的摘要统计(设备动作) + + Returns: + dict: ROS2动作结果格式 {"return_info": str, "success": bool} + """ + try: + # 确保_channels已初始化 + if not hasattr(self, '_channels') or not self._channels: + self._channels = self._build_channel_map() + + summary = {} + for channel in self._channels: + devid = channel.devid + summary[devid] = summary.get(devid, 0) + 1 + + result_info = json.dumps(summary, ensure_ascii=False) + success_msg = f"设备摘要统计: {result_info}" + if self._ros_node: + self._ros_node.lab_logger().info(success_msg) + return {"return_info": result_info, "success": True} + + except Exception as e: + error_msg = f"获取设备摘要失败: {str(e)}" + if self._ros_node: + self._ros_node.lab_logger().error(error_msg) + return {"return_info": error_msg, "success": False} + + def test_connection_action(self) -> dict: + """ + 测试TCP连接(设备动作) + + Returns: + dict: ROS2动作结果格式 {"return_info": str, "success": bool} + """ + try: + is_connected = self.test_connection() + if is_connected: + success_msg = f"TCP连接测试成功: {self.ip}:{self.port}" + if self._ros_node: + self._ros_node.lab_logger().info(success_msg) + return {"return_info": success_msg, "success": True} + else: + error_msg = f"TCP连接测试失败: {self.ip}:{self.port}" + if self._ros_node: + self._ros_node.lab_logger().warning(error_msg) + return {"return_info": error_msg, "success": False} + + except Exception as e: + error_msg = f"连接测试异常: {str(e)}" + if self._ros_node: + self._ros_node.lab_logger().error(error_msg) + return {"return_info": error_msg, "success": False} + + def print_status_summary_action(self) -> dict: + """ + 打印状态摘要(设备动作) + + Returns: + dict: ROS2动作结果格式 {"return_info": str, "success": bool} + """ + try: + self.print_status_summary() + success_msg = "状态摘要已打印到控制台" + if self._ros_node: + self._ros_node.lab_logger().info(success_msg) + return {"return_info": success_msg, "success": True} + + except Exception as e: + error_msg = f"打印状态摘要失败: {str(e)}" + if self._ros_node: + self._ros_node.lab_logger().error(error_msg) + return {"return_info": error_msg, "success": False} + + def query_plate_action(self, plate_id: str = "P1") -> dict: + """ + 查询指定盘的详细信息(设备动作) + + Args: + plate_id: 盘号标识,如"P1"或"P2" + + Returns: + dict: ROS2动作结果格式,包含指定盘的详细通道信息 + """ + try: + # 解析盘号 + if plate_id.upper() == "P1": + plate_num = 1 + elif plate_id.upper() == "P2": + plate_num = 2 + else: + error_msg = f"无效的盘号: {plate_id},仅支持P1或P2" + if self._ros_node: + self._ros_node.lab_logger().warning(error_msg) + return {"return_info": error_msg, "success": False} + + # 获取指定盘的详细信息 + plate_detail = self._get_plate_detail_info(plate_num) + + success_msg = f"成功获取{plate_id}盘详细信息,包含{len(plate_detail['channels'])}个通道" + if self._ros_node: + self._ros_node.lab_logger().info(success_msg) + + return { + "return_info": success_msg, + "success": True, + "plate_data": plate_detail + } + + except Exception as e: + error_msg = f"查询盘{plate_id}详细信息失败: {str(e)}" + if self._ros_node: + self._ros_node.lab_logger().error(error_msg) + return {"return_info": error_msg, "success": False} + + def _get_plate_detail_info(self, plate_num: int) -> dict: + """ + 获取指定盘的详细信息,包含设备ID、子设备ID、通道ID映射 + + Args: + plate_num: 盘号 (1 或 2) + + Returns: + dict: 包含详细通道信息的字典 + """ + # 获取最新的通道状态数据 + channel_status_data = self.channel_status + subunits = channel_status_data.get('subunits', {}) + + if plate_num == 1: + devid = 1 + subdevid_range = range(1, 6) # 子设备ID 1-5 + elif plate_num == 2: + devid = 1 + subdevid_range = range(6, 11) # 子设备ID 6-10 + else: + raise ValueError("盘号必须是1或2") + + channels = [] + + # 直接从subunits数据构建通道信息,而不依赖资源状态 + for subdev_id in subdevid_range: + status_row = subunits.get(subdev_id, {}) + + for chl_id in range(1, 9): # chlid 1-8 + try: + # 计算在5×8网格中的位置 + if plate_num == 1: + row_idx = (subdev_id - 1) # 0-4 (对应A-E) + else: # plate_num == 2 + row_idx = (subdev_id - 6) # 0-4 (subdevid 6->0, 7->1, ..., 10->4) (对应A-E) + + col_idx = (chl_id - 1) # 0-7 (对应1-8) + position = f"{self.LETTERS[row_idx]}{col_idx + 1}" + name = f"P{plate_num}_{position}" + + # 从subunits直接获取通道状态数据 + status_channel = status_row.get(chl_id, {}) + + # 提取metrics数据(如果存在) + metrics = status_channel.get('metrics', {}) + + channel_info = { + 'name': name, + 'devid': devid, + 'subdevid': subdev_id, + 'chlid': chl_id, + 'position': position, + 'status': status_channel.get('state', 'unknown'), + 'color': status_channel.get('color', self.STATUS_COLOR['unknown']), + 'voltage': metrics.get('voltage_V', 0.0), + 'current': metrics.get('current_A', 0.0), + 'time': metrics.get('totaltime_s', 0.0) + } + + channels.append(channel_info) + + except (ValueError, IndexError, KeyError): + # 如果解析失败,跳过该通道 + continue + + # 按位置排序(先按行,再按列) + channels.sort(key=lambda x: (x['subdevid'], x['chlid'])) + + # 统计状态 + stats = {s: 0 for s in self.STATUS_SET | {"unknown"}} + for channel in channels: + stats[channel['status']] += 1 + + return { + 'plate_id': f"P{plate_num}", + 'plate_num': plate_num, + 'devid': devid, + 'subdevid_range': list(subdevid_range), + 'total_channels': len(channels), + 'stats': stats, + 'channels': channels + } + + # ======================== + # TCP通信和协议处理 + # ======================== + + def _build_channel_map(self) -> List['ChannelKey']: + """构建全量通道映射(720个通道)""" + channels = [] + + # devid 1-7: subdevid 1-10, chlid 1-8 + for devid in range(1, 8): + for sub in range(1, 11): + for ch in range(1, 9): + channels.append(ChannelKey(devid, sub, ch)) + + # devid 8: subdevid 11-20, chlid 1-8 + for sub in range(11, 21): + for ch in range(1, 9): + channels.append(ChannelKey(8, sub, ch)) + + # devid 86: subdevid 1-10, chlid 1-8 + for sub in range(1, 11): + for ch in range(1, 9): + channels.append(ChannelKey(86, sub, ch)) + + return channels + + def _query_all_channels(self) -> Dict['ChannelKey', dict]: + """执行TCP查询获取所有通道状态""" + try: + req_xml = self._build_inquire_xml() + + with socket.create_connection((self.ip, self.port), timeout=self.timeout) as sock: + sock.settimeout(self.timeout) + sock.sendall(req_xml) + response = self._recv_until(sock) + + return self._parse_inquire_resp(response) + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"查询通道状态失败: {e}") + else: + print(f"查询通道状态失败: {e}") + return {} + + def _build_inquire_xml(self) -> bytes: + """构造inquire请求XML""" + lines = [ + '', + '', + 'inquire', + f'' + ] + + for c in self._channels: + lines.append( + f'true' + ) + + lines.extend(['', '']) + xml_text = "\n".join(lines) + return xml_text.encode("utf-8") + self.REQ_END + + def _recv_until(self, sock: socket.socket, end_token: bytes = None, + alt_close_tag: bytes = b"") -> bytes: + """接收TCP响应数据""" + if end_token is None: + end_token = self.REQ_END + + buf = bytearray() + while True: + chunk = sock.recv(8192) + if not chunk: + break + buf.extend(chunk) + if end_token in buf: + cut = buf.rfind(end_token) + return bytes(buf[:cut]) + if alt_close_tag in buf: + cut = buf.rfind(alt_close_tag) + len(alt_close_tag) + return bytes(buf[:cut]) + return bytes(buf) + + def _parse_inquire_resp(self, xml_bytes: bytes) -> Dict['ChannelKey', dict]: + """解析inquire_resp响应XML""" + mapping = {} + + try: + xml_text = xml_bytes.decode("utf-8", errors="ignore").strip() + if not xml_text: + return mapping + + root = ET.fromstring(xml_text) + cmd = root.findtext("cmd", default="").strip() + + if cmd != "inquire_resp": + return mapping + + list_node = root.find("list") + if list_node is None: + return mapping + + for node in list_node.findall("inquire"): + # 解析 dev="27-1-1-1-0" + dev = node.get("dev", "") + parts = dev.split("-") + # 容错:至少需要 5 段 + if len(parts) < 5: + continue + try: + devtype = int(parts[0]) # 未使用,但解析以校验正确性 + devid = int(parts[1]) + subdevid = int(parts[2]) + chlid = int(parts[3]) + aux = int(parts[4]) + except ValueError: + continue + + key = ChannelKey(devid, subdevid, chlid) + + # 提取属性,带类型转换与缺省值 + def fget(name: str, cast, default): + v = node.get(name) + if v is None or v == "": + return default + try: + return cast(v) + except Exception: + return default + + workstatus = (node.get("workstatus", "") or "").lower() + if workstatus not in self.STATUS_SET: + workstatus = "unknown" + + current = fget("current", float, 0.0) + voltage = fget("voltage", float, 0.0) + capacity = fget("capacity", float, 0.0) + energy = fget("energy", float, 0.0) + totaltime = fget("totaltime", float, 0.0) + relativetime = fget("relativetime", float, 0.0) + open_close = fget("open_or_close", int, 0) + cycle_id = fget("cycle_id", int, 0) + step_id = fget("step_id", int, 0) + step_type = node.get("step_type", "") or "" + log_code = node.get("log_code", "") or "" + barcode = node.get("barcode") + + mapping[key] = { + "state": workstatus, + "color": self.STATUS_COLOR.get(workstatus, self.STATUS_COLOR["unknown"]), + "current_A": current, + "voltage_V": voltage, + "capacity_Ah": capacity, + "energy_Wh": energy, + "totaltime_s": totaltime, + "relativetime_s": relativetime, + "open_or_close": open_close, + "step_type": step_type, + "cycle_id": cycle_id, + "step_id": step_id, + "log_code": log_code, + **({"barcode": barcode} if barcode is not None else {}), + } + + except Exception as e: + if self._ros_node: + self._ros_node.lab_logger().error(f"解析XML响应失败: {e}") + else: + print(f"解析XML响应失败: {e}") + + return mapping + + def _group_by_devid(self, status_map: Dict['ChannelKey', dict]) -> Dict[int, Dict]: + """按设备ID分组状态数据""" + result = {} + + for key, val in status_map.items(): + if key.devid not in result: + result[key.devid] = { + "stats": {s: 0 for s in self.STATUS_SET | {"unknown"}}, + "subunits": {} + } + + dev = result[key.devid] + state = val.get("state", "unknown") + dev["stats"][state] = dev["stats"].get(state, 0) + 1 + + subunits = dev["subunits"] + if key.subdevid not in subunits: + subunits[key.subdevid] = {} + + subunits[key.subdevid][key.chlid] = { + "state": state, + "color": val.get("color", self.STATUS_COLOR["unknown"]), + "open_or_close": val.get("open_or_close", 0), + "metrics": { + "voltage_V": val.get("voltage_V", 0.0), + "current_A": val.get("current_A", 0.0), + "capacity_Ah": val.get("capacity_Ah", 0.0), + "energy_Wh": val.get("energy_Wh", 0.0), + "totaltime_s": val.get("totaltime_s", 0.0), + "relativetime_s": val.get("relativetime_s", 0.0) + }, + "meta": { + "step_type": val.get("step_type", ""), + "cycle_id": val.get("cycle_id", 0), + "step_id": val.get("step_id", 0), + "log_code": val.get("log_code", "") + } + } + + return result + + +# ======================== +# 示例和测试代码 +# ======================== +def main(): + """测试和演示设备类的使用(支持2盘80颗电池)""" + print("=== 新威电池测试系统设备类演示(2盘80颗电池) ===") + + # 创建设备实例 + bts = NewareBatteryTestSystem() + + # 创建一个模拟的ROS节点用于初始化 + class MockRosNode: + def lab_logger(self): + import logging + return logging.getLogger(__name__) + + def update_resource(self, *args, **kwargs): + pass # 空实现,避免ROS调用错误 + + # 调用post_init进行正确的初始化 + mock_ros_node = MockRosNode() + bts.post_init(mock_ros_node) + + # 测试连接 + print(f"\n1. 连接测试:") + print(f" 连接信息: {bts.connection_info}") + if bts.test_connection(): + print(" ✓ TCP连接正常") + else: + print(" ✗ TCP连接失败") + return + + # 获取设备摘要 + print(f"\n2. 设备摘要:") + print(f" 总通道数: {bts.total_channels}") + summary_result = bts.get_device_summary() + if summary_result["success"]: + # 直接解析return_info,因为它就是JSON字符串 + summary = json.loads(summary_result["return_info"]) + for devid, count in summary.items(): + print(f" 设备ID {devid}: {count} 个通道") + else: + print(f" 获取设备摘要失败: {summary_result['return_info']}") + + # 显示物料管理系统信息 + print(f"\n3. 物料管理系统:") + print(f" 第1盘资源数: {len(bts.station_resources_plate1)}") + print(f" 第2盘资源数: {len(bts.station_resources_plate2)}") + print(f" 总资源数: {len(bts.station_resources)}") + + # 获取实时状态 + print(f"\n4. 获取通道状态:") + try: + bts.print_status_summary() + except Exception as e: + print(f" 获取状态失败: {e}") + + # 分别获取两盘的状态 + print(f"\n5. 分盘状态统计:") + try: + plate_status_data = bts.plate_status + for plate_num in [1, 2]: + plate_key = f"plate{plate_num}" # 修正键名格式:plate1, plate2 + if plate_key in plate_status_data: + plate_info = plate_status_data[plate_key] + print(f" 第{plate_num}盘:") + print(f" 总位置数: {plate_info['total_positions']}") + print(f" 活跃位置数: {plate_info['active_positions']}") + for state, count in plate_info['stats'].items(): + if count > 0: + print(f" {state}: {count} 个位置") + else: + print(f" 第{plate_num}盘: 无数据") + except Exception as e: + print(f" 获取分盘状态失败: {e}") + + # 导出JSON + print(f"\n6. 导出状态数据:") + result = bts.export_status_json("demo_2plate_status.json") + if result["success"]: + print(" ✓ 状态数据已导出到 demo_2plate_status.json") + else: + print(" ✗ 导出失败") + + +if __name__ == "__main__": + main() diff --git a/unilabos/registry/devices/neware_battery_test_system.yaml b/unilabos/registry/devices/neware_battery_test_system.yaml new file mode 100644 index 00000000..3dd7568d --- /dev/null +++ b/unilabos/registry/devices/neware_battery_test_system.yaml @@ -0,0 +1,344 @@ +neware_battery_test_system: + category: + - neware_battery_test_system + class: + action_value_mappings: + auto-post_init: + feedback: {} + goal: {} + goal_default: + ros_node: null + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + ros_node: + type: string + required: + - ros_node + type: object + result: {} + required: + - goal + title: post_init参数 + type: object + type: UniLabJsonCommand + auto-print_status_summary: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: print_status_summary参数 + type: object + type: UniLabJsonCommand + auto-test_connection: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: test_connection参数 + type: object + type: UniLabJsonCommand + export_status_json: + feedback: {} + goal: + filepath: filepath + goal_default: + filepath: bts_status.json + handles: {} + result: + return_info: return_info + success: success + schema: + description: 导出当前状态数据到JSON文件 + properties: + feedback: {} + goal: + properties: + filepath: + default: bts_status.json + description: 输出JSON文件路径 + type: string + required: [] + type: object + result: + properties: + return_info: + description: 导出操作结果信息 + type: string + success: + description: 导出是否成功 + type: boolean + required: + - return_info + - success + type: object + required: + - goal + type: object + type: UniLabJsonCommand + get_device_summary: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: + return_info: return_info + success: success + schema: + description: 获取设备级别的摘要统计信息 + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: + properties: + return_info: + description: 设备摘要信息JSON格式 + type: string + success: + description: 查询是否成功 + type: boolean + required: + - return_info + - success + type: object + required: + - goal + type: object + type: UniLabJsonCommand + get_plate_status: + feedback: {} + goal: + plate_num: plate_num + goal_default: + plate_num: 1 + handles: {} + result: + return_info: return_info + success: success + schema: + description: 获取指定盘(1或2)的电池状态信息 + properties: + feedback: {} + goal: + properties: + plate_num: + description: 盘号 (1 或 2) + maximum: 2 + minimum: 1 + type: integer + required: + - plate_num + type: object + result: + properties: + return_info: + description: 盘状态信息JSON格式 + type: string + success: + description: 查询是否成功 + type: boolean + required: + - return_info + - success + type: object + required: + - goal + type: object + type: UniLabJsonCommand + print_status_summary_action: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: + return_info: return_info + success: success + schema: + description: 打印通道状态摘要信息到控制台 + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: + properties: + return_info: + description: 打印操作结果信息 + type: string + success: + description: 打印是否成功 + type: boolean + required: + - return_info + - success + type: object + required: + - goal + type: object + type: UniLabJsonCommand + query_plate_action: + feedback: {} + goal: + string: plate_id + goal_default: + string: '' + handles: {} + result: {} + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: StrSingleInput_Feedback + type: object + goal: + properties: + string: + type: string + required: + - string + title: StrSingleInput_Goal + type: object + result: + properties: + return_info: + type: string + success: + type: boolean + required: + - return_info + - success + title: StrSingleInput_Result + type: object + required: + - goal + title: StrSingleInput + type: object + type: StrSingleInput + test_connection_action: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: + return_info: return_info + success: success + schema: + description: 测试与电池测试系统的TCP连接 + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: + properties: + return_info: + description: 连接测试结果信息 + type: string + success: + description: 连接测试是否成功 + type: boolean + required: + - return_info + - success + type: object + required: + - goal + type: object + type: UniLabJsonCommand + module: unilabos.devices.battery.neware_battery_test_system:NewareBatteryTestSystem + status_types: + channel_status: dict + connection_info: dict + device_summary: dict + plate_status: dict + status: str + total_channels: int + type: python + config_info: [] + description: 新威电池测试系统驱动,支持720个通道的电池测试状态监控和数据导出。通过TCP通信实现远程控制,包含完整的物料管理系统,支持2盘电池的状态映射和监控。 + handles: [] + icon: '' + init_param_schema: + config: + properties: + devtype: + type: string + ip: + type: string + machine_id: + default: 1 + type: integer + port: + type: integer + size_x: + default: 500.0 + type: number + size_y: + default: 500.0 + type: number + size_z: + default: 2000.0 + type: number + timeout: + type: integer + required: [] + type: object + data: + properties: + channel_status: + type: object + connection_info: + type: object + device_summary: + type: object + plate_status: + type: object + status: + type: string + total_channels: + type: integer + required: + - status + - channel_status + - connection_info + - total_channels + - plate_status + - device_summary + type: object + version: 1.0.0 From a8cc02a1267ba453f45ea1fa0081e7074839919a Mon Sep 17 00:00:00 2001 From: Junhan Chang Date: Thu, 25 Sep 2025 20:36:52 +0800 Subject: [PATCH 08/17] add bioyond studio draft --- .../workstation/bioyond_studio/bioyond_rpc.py | 1058 +++++++++++++++++ .../workstation/bioyond_studio/experiment.py | 398 +++++++ .../workstation/bioyond_studio/station.py | 334 ++++++ .../devices/reaction_station_bioyong.yaml | 481 ++++++++ 4 files changed, 2271 insertions(+) create mode 100644 unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py create mode 100644 unilabos/devices/workstation/bioyond_studio/experiment.py create mode 100644 unilabos/devices/workstation/bioyond_studio/station.py create mode 100644 unilabos/registry/devices/reaction_station_bioyong.yaml diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py b/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py new file mode 100644 index 00000000..9f46dfcf --- /dev/null +++ b/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py @@ -0,0 +1,1058 @@ +# bioyond_rpc.py +""" +BioyondV1RPC类定义 - 包含所有RPC接口和业务逻辑 +""" + +from enum import Enum +from datetime import datetime, timezone +from unilabos.device_comms.rpc import BaseRequest +from typing import Optional, List, Dict, Any +import json +from config import WORKFLOW_TO_SECTION_MAP, WORKFLOW_STEP_IDS, LOCATION_MAPPING + + +class SimpleLogger: + """简单的日志记录器""" + def info(self, msg): print(f"[INFO] {msg}") + def error(self, msg): print(f"[ERROR] {msg}") + def debug(self, msg): print(f"[DEBUG] {msg}") + def warning(self, msg): print(f"[WARNING] {msg}") + def critical(self, msg): print(f"[CRITICAL] {msg}") + + +class MachineState(Enum): + INITIAL = 0 + STOPPED = 1 + RUNNING = 2 + PAUSED = 3 + ERROR_PAUSED = 4 + ERROR_STOPPED = 5 + + +class MaterialType(Enum): + Consumables = 0 + Sample = 1 + Reagent = 2 + Product = 3 + + +class BioyondV1RPC(BaseRequest): + def __init__(self, config): + super().__init__() + print("开始初始化") + self.config = config + self.api_key = config["api_key"] + self.host = config["api_host"] + self._logger = SimpleLogger() + self.is_running = False + self.workflow_mappings = {} + self.workflow_sequence = [] + self.pending_task_params = [] + self.material_cache = {} + self._load_material_cache() + + if "workflow_mappings" in config: + self._set_workflow_mappings(config["workflow_mappings"]) + + def _set_workflow_mappings(self, mappings: Dict[str, str]): + self.workflow_mappings = mappings + print(f"设置工作流映射配置: {mappings}") + + def _get_workflow(self, web_workflow_name: str) -> str: + if web_workflow_name not in self.workflow_mappings: + print(f"未找到工作流映射配置: {web_workflow_name}") + return "" + workflow_id = self.workflow_mappings[web_workflow_name] + print(f"获取工作流: {web_workflow_name} -> {workflow_id}") + return workflow_id + + def process_web_workflows(self, json_str: str) -> Dict[str, str]: + try: + data = json.loads(json_str) + web_workflow_list = data.get("web_workflow_list", []) + except json.JSONDecodeError: + print(f"无效的JSON字符串: {json_str}") + return {} + + result = {} + self.workflow_sequence = [] + + for web_name in web_workflow_list: + workflow_id = self._get_workflow(web_name) + if workflow_id: + result[web_name] = workflow_id + self.workflow_sequence.append(workflow_id) + else: + print(f"无法获取工作流ID: {web_name}") + + print(f"工作流执行顺序: {self.workflow_sequence}") + return result + + def get_workflow_sequence(self) -> List[str]: + id_to_name = {workflow_id: name for name, workflow_id in self.workflow_mappings.items()} + workflow_names = [] + for workflow_id in self.workflow_sequence: + workflow_names.append(id_to_name.get(workflow_id, workflow_id)) + return workflow_names + + def append_to_workflow_sequence(self, json_str: str) -> bool: + try: + data = json.loads(json_str) + web_workflow_name = data.get("web_workflow_name", "") + except: + return False + + workflow_id = self._get_workflow(web_workflow_name) + if workflow_id: + self.workflow_sequence.append(workflow_id) + print(f"添加工作流到执行顺序: {web_workflow_name} -> {workflow_id}") + + def set_workflow_sequence(self, json_str: str) -> List[str]: + try: + data = json.loads(json_str) + web_workflow_names = data.get("web_workflow_names", []) + except: + return [] + + sequence = [] + for web_name in web_workflow_names: + workflow_id = self._get_workflow(web_name) + if workflow_id: + sequence.append(workflow_id) + + self.workflow_sequence = sequence + print(f"设置工作流执行顺序: {self.workflow_sequence}") + return self.workflow_sequence.copy() + + def get_all_workflows(self) -> Dict[str, str]: + return self.workflow_mappings.copy() + + def clear_workflows(self): + self.workflow_sequence = [] + print("清空工作流执行顺序") + + def get_current_time_iso8601(self) -> str: + current_time = datetime.now(timezone.utc).isoformat(timespec='milliseconds') + return current_time.replace("+00:00", "Z") + + # 物料查询接口 + def stock_material(self, json_str: str) -> list: + try: + params = json.loads(json_str) + except: + return [] + + response = self.post( + url=f'{self.host}/api/lims/storage/stock-material', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response or response['code'] != 1: + return [] + return response.get("data", []) + + # 工作流列表查询 + def query_workflow(self, json_str: str) -> dict: + try: + params = json.loads(json_str) + except: + return {} + + response = self.post( + url=f'{self.host}/api/lims/workflow/work-flow-list', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + # 工作流步骤查询接口 + def workflow_step_query(self, json_str: str) -> dict: + try: + data = json.loads(json_str) + workflow_id = data.get("workflow_id", "") + except: + return {} + + response = self.post( + url=f'{self.host}/api/lims/workflow/sub-workflow-step-parameters', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": workflow_id, + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + # 任务推送接口 + def create_order(self, json_str: str) -> dict: + try: + params = json.loads(json_str) + except Exception as e: + result = str({"success": False, "error": f"create_order:处理JSON时出错: {str(e)}", "method": "create_order"}) + return result + + print('===============', json.dumps(params)) + + request_params = { + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + } + + response = self.post( + url=f'{self.host}/api/lims/order/order', + params=request_params) + + if response['code'] != 1: + print(f"create order error: {response.get('message')}") + + print(f"create order data: {response['data']}") + result = str(response.get("data", {})) + return result + + # 查询任务列表 + def order_query(self, json_str: str) -> dict: + try: + params = json.loads(json_str) + except: + return {} + + response = self.post( + url=f'{self.host}/api/lims/order/order-list', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + # 任务明细查询 + def order_report(self, json_str: str) -> dict: + try: + data = json.loads(json_str) + order_id = data.get("order_id", "") + except: + return {} + + response = self.post( + url=f'{self.host}/api/lims/order/order-report', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": order_id, + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + # 任务取出接口 + def order_takeout(self, json_str: str) -> int: + try: + data = json.loads(json_str) + params = { + "orderId": data.get("order_id", ""), + "preintakeId": data.get("preintake_id", "") + } + except: + return 0 + + response = self.post( + url=f'{self.host}/api/lims/order/order-takeout', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params, + }) + + if not response or response['code'] != 1: + return 0 + return response.get("code", 0) + + # 设备列表查询 + def device_list(self, json_str: str = "") -> list: + device_no = None + if json_str: + try: + data = json.loads(json_str) + device_no = data.get("device_no", None) + except: + pass + + url = f'{self.host}/api/lims/device/device-list' + if device_no: + url += f'/{device_no}' + + response = self.post( + url=url, + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + return [] + return response.get("data", []) + + # 设备操作 + def device_operation(self, json_str: str) -> int: + try: + data = json.loads(json_str) + params = { + "deviceNo": data.get("device_no", ""), + "operationType": data.get("operation_type", 0), + "operationParams": data.get("operation_params", {}) + } + except: + return 0 + + response = self.post( + url=f'{self.host}/api/lims/device/device-operation', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params, + }) + + if not response or response['code'] != 1: + return 0 + return response.get("code", 0) + + # 调度器状态查询 + def scheduler_status(self) -> dict: + response = self.post( + url=f'{self.host}/api/lims/scheduler/scheduler-status', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + # 调度器启动 + def scheduler_start(self) -> int: + response = self.post( + url=f'{self.host}/api/lims/scheduler/start', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + return 0 + return response.get("code", 0) + + # 调度器暂停 + def scheduler_pause(self) -> int: + response = self.post( + url=f'{self.host}/api/lims/scheduler/scheduler-pause', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + return 0 + return response.get("code", 0) + + # 调度器继续 + def scheduler_continue(self) -> int: + response = self.post( + url=f'{self.host}/api/lims/scheduler/scheduler-continue', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + return 0 + return response.get("code", 0) + + # 调度器停止 + def scheduler_stop(self) -> int: + response = self.post( + url=f'{self.host}/api/lims/scheduler/scheduler-stop', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + return 0 + return response.get("code", 0) + + # 调度器重置 + def scheduler_reset(self) -> int: + response = self.post( + url=f'{self.host}/api/lims/scheduler/scheduler-reset', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + return 0 + return response.get("code", 0) + + # 取消任务 + def cancel_order(self, json_str: str) -> bool: + try: + data = json.loads(json_str) + order_id = data.get("order_id", "") + except: + return False + + response = self.post( + url=f'{self.host}/api/lims/order/cancel-order', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": order_id, + }) + + if not response or response['code'] != 1: + return False + return True + + # 获取可拼接工作流 + def query_split_workflow(self) -> list: + response = self.post( + url=f'{self.host}/api/lims/workflow/split-workflow-list', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + }) + + if not response or response['code'] != 1: + return [] + return str(response.get("data", {})) + + # 合并工作流 + def merge_workflow(self, json_str: str) -> dict: + try: + data = json.loads(json_str) + params = { + "name": data.get("name", ""), + "workflowIds": data.get("workflow_ids", []) + } + except: + return {} + + response = self.post( + url=f'{self.host}/api/lims/workflow/merge-workflow', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params, + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + # 合并当前工作流序列 + def merge_sequence_workflow(self, json_str: str) -> dict: + try: + data = json.loads(json_str) + name = data.get("name", "合并工作流") + except: + return {} + + if not self.workflow_sequence: + print("工作流序列为空,无法合并") + return {} + + params = { + "name": name, + "workflowIds": self.workflow_sequence + } + + response = self.post( + url=f'{self.host}/api/lims/workflow/merge-workflow', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params, + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + # 发布任务 + def process_and_execute_workflow(self, workflow_name: str, task_name: str) -> dict: + web_workflow_list = self.get_workflow_sequence() + workflow_name = workflow_name + + pending_params_backup = self.pending_task_params.copy() + print(f"保存pending_task_params副本,共{len(pending_params_backup)}个参数") + + # 1. 处理网页工作流列表 + print(f"处理网页工作流列表: {web_workflow_list}") + web_workflow_json = json.dumps({"web_workflow_list": web_workflow_list}) + workflows_result = self.process_web_workflows(web_workflow_json) + + if not workflows_result: + error_msg = "处理网页工作流列表失败" + print(error_msg) + result = str({"success": False, "error": f"process_and_execute_workflow:{error_msg}", "method": "process_and_execute_workflow", "step": "process_web_workflows"}) + return result + + # 2. 合并工作流序列 + print(f"合并工作流序列,名称: {workflow_name}") + merge_json = json.dumps({"name": workflow_name}) + merged_workflow = self.merge_sequence_workflow(merge_json) + print(f"合并工作流序列结果: {merged_workflow}") + + if not merged_workflow: + error_msg = "合并工作流序列失败" + print(error_msg) + result = str({"success": False, "error": f"process_and_execute_workflow:{error_msg}", "method": "process_and_execute_workflow", "step": "merge_sequence_workflow"}) + return result + + # 3. 合并所有参数并创建任务 + workflow_name = merged_workflow.get("name", "") + workflow_id = merged_workflow.get("subWorkflows", [{}])[0].get("id", "") + print(f"使用工作流创建任务: {workflow_name} (ID: {workflow_id})") + + workflow_query_json = json.dumps({"workflow_id": workflow_id}) + workflow_params_structure = self.workflow_step_query(workflow_query_json) + + self.pending_task_params = pending_params_backup + print(f"恢复pending_task_params,共{len(self.pending_task_params)}个参数") + + param_values = self.generate_task_param_values(workflow_params_structure) + + task_params = [{ + "orderCode": f"BSO{self.get_current_time_iso8601().replace('-', '').replace('T', '').replace(':', '').replace('.', '')[:14]}", + "orderName": f"实验-{self.get_current_time_iso8601()[:10].replace('-', '')}", + "workFlowId": workflow_id, + "borderNumber": 1, + "paramValues": param_values, + "extendProperties": "" + }] + + task_json = json.dumps(task_params) + print(f"创建任务参数: {type(task_json)}") + result = self.create_order(task_json) + + if not result: + error_msg = "创建任务失败" + print(error_msg) + result = str({"success": False, "error": f"process_and_execute_workflow:{error_msg}", "method": "process_and_execute_workflow", "step": "create_order"}) + return result + + print(f"任务创建成功: {result}") + self.pending_task_params.clear() + print("已清空pending_task_params") + + return { + "success": True, + "workflow": {"name": workflow_name, "id": workflow_id}, + "task": result, + "method": "process_and_execute_workflow" + } + + # 生成任务参数 + def generate_task_param_values(self, workflow_params_structure): + if not workflow_params_structure: + print("workflow_params_structure为空") + return {} + + data = workflow_params_structure + + # 从pending_task_params中提取实际参数值,按DisplaySectionName和Key组织 + pending_params_by_section = {} + print(f"开始处理pending_task_params,共{len(self.pending_task_params)}个任务参数组") + + # 获取工作流执行顺序,用于按顺序匹配参数 + workflow_sequence = self.get_workflow_sequence() + print(f"工作流执行顺序: {workflow_sequence}") + + workflow_index = 0 + + for i, task_param in enumerate(self.pending_task_params): + if 'param_values' in task_param: + print(f"处理第{i+1}个任务参数组,包含{len(task_param['param_values'])}个步骤") + + if workflow_index < len(workflow_sequence): + current_workflow = workflow_sequence[workflow_index] + section_name = WORKFLOW_TO_SECTION_MAP.get(current_workflow) + print(f" 匹配到工作流: {current_workflow} -> {section_name}") + workflow_index += 1 + else: + print(f" 警告: 参数组{i+1}超出了工作流序列范围") + continue + + if not section_name: + print(f" 警告: 工作流{current_workflow}没有对应的DisplaySectionName") + continue + + if section_name not in pending_params_by_section: + pending_params_by_section[section_name] = {} + + for step_id, param_list in task_param['param_values'].items(): + print(f" 步骤ID: {step_id},参数数量: {len(param_list)}") + + for param_item in param_list: + key = param_item.get('Key', '') + value = param_item.get('Value', '') + m = param_item.get('m', 0) + n = param_item.get('n', 0) + print(f" 参数: {key} = {value} (m={m}, n={n}) -> 分组到{section_name}") + + param_key = f"{section_name}.{key}" + if param_key not in pending_params_by_section[section_name]: + pending_params_by_section[section_name][param_key] = [] + + pending_params_by_section[section_name][param_key].append({ + 'value': value, + 'm': m, + 'n': n + }) + + print(f"pending_params_by_section构建完成,包含{len(pending_params_by_section)}个分组") + + # 收集所有参数,过滤TaskDisplayable为0的项 + filtered_params = [] + + for step_id, step_info in data.items(): + if isinstance(step_info, list): + for step_item in step_info: + param_list = step_item.get("parameterList", []) + for param in param_list: + if param.get("TaskDisplayable") == 0: + continue + + param_with_step = param.copy() + param_with_step['step_id'] = step_id + param_with_step['step_name'] = step_item.get("name", "") + param_with_step['step_m'] = step_item.get("m", 0) + param_with_step['step_n'] = step_item.get("n", 0) + filtered_params.append(param_with_step) + + # 按DisplaySectionIndex排序 + filtered_params.sort(key=lambda x: x.get('DisplaySectionIndex', 0)) + + # 生成参数映射 + param_mapping = {} + step_params = {} + for param in filtered_params: + step_id = param['step_id'] + if step_id not in step_params: + step_params[step_id] = [] + step_params[step_id].append(param) + + # 为每个步骤生成参数 + for step_id, params in step_params.items(): + param_list = [] + for param in params: + key = param.get('Key', '') + display_section_index = param.get('DisplaySectionIndex', 0) + step_m = param.get('step_m', 0) + step_n = param.get('step_n', 0) + + section_name = param.get('DisplaySectionName', '') + param_key = f"{section_name}.{key}" + + if section_name in pending_params_by_section and param_key in pending_params_by_section[section_name]: + pending_param_list = pending_params_by_section[section_name][param_key] + if pending_param_list: + pending_param = pending_param_list[0] + value = pending_param['value'] + m = step_m + n = step_n + print(f" 匹配成功: {section_name}.{key} = {value} (m={m}, n={n})") + pending_param_list.pop(0) + else: + value = "1" + m = step_m + n = step_n + print(f" 匹配失败: {section_name}.{key},参数列表为空,使用默认值 = {value}") + else: + value = "1" + m = display_section_index + n = step_n + print(f" 匹配失败: {section_name}.{key},使用默认值 = {value} (m={m}, n={n})") + + param_item = { + "m": m, + "n": n, + "key": key, + "value": str(value).strip() + } + param_list.append(param_item) + + if param_list: + param_mapping[step_id] = param_list + + print(f"生成任务参数值,包含 {len(param_mapping)} 个步骤") + return param_mapping + + # 工作流方法 + def reactor_taken_out(self): + """反应器取出""" + self.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_out"}') + reactor_taken_out_params = {"param_values": {}} + self.pending_task_params.append(reactor_taken_out_params) + print(f"成功添加反应器取出工作流") + print(f"当前队列长度: {len(self.pending_task_params)}") + return json.dumps({"suc": True}) + + def reactor_taken_in(self, assign_material_name: str, cutoff: str = "900000", temperature: float = -10.00): + """反应器放入""" + self.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_in"}') + material_id = self._get_material_id_by_name(assign_material_name) + + if isinstance(temperature, str): + temperature = float(temperature) + + step_id = WORKFLOW_STEP_IDS["reactor_taken_in"]["config"] + reactor_taken_in_params = { + "param_values": { + step_id: [ + {"m": 0, "n": 3, "Key": "cutoff", "Value": cutoff}, + {"m": 0, "n": 3, "Key": "temperature", "Value": f"{temperature:.2f}"}, + {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id} + ] + } + } + + self.pending_task_params.append(reactor_taken_in_params) + print(f"成功添加反应器放入参数: material={assign_material_name}->ID:{material_id}, cutoff={cutoff}, temp={temperature:.2f}") + print(f"当前队列长度: {len(self.pending_task_params)}") + return json.dumps({"suc": True}) + + def solid_feeding_vials(self, material_id: str, time: str = "0", torque_variation: str = "1", + assign_material_name: str = None, temperature: float = 25.00): + """固体进料小瓶""" + self.append_to_workflow_sequence('{"web_workflow_name": "Solid_feeding_vials"}') + material_id_m = self._get_material_id_by_name(assign_material_name) + + if isinstance(temperature, str): + temperature = float(temperature) + + feeding_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["feeding"] + observe_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["observe"] + + solid_feeding_vials_params = { + "param_values": { + feeding_id: [ + {"m": 0, "n": 3, "Key": "materialId", "Value": material_id}, + {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id_m} + ], + observe_id: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } + } + + self.pending_task_params.append(solid_feeding_vials_params) + print(f"成功添加固体进料小瓶参数: material_id={material_id}, time={time}min, temp={temperature:.2f}°C") + print(f"当前队列长度: {len(self.pending_task_params)}") + return json.dumps({"suc": True}) + + def liquid_feeding_vials_non_titration(self, volumeFormula: str, assign_material_name: str, + titration_type: str = "1", time: str = "0", + torque_variation: str = "1", temperature: float = 25.00): + """液体进料小瓶(非滴定)""" + self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_vials(non-titration)"}') + material_id = self._get_material_id_by_name(assign_material_name) + + if isinstance(temperature, str): + temperature = float(temperature) + + liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["liquid"] + observe_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["observe"] + + params = { + "param_values": { + liquid_id: [ + {"m": 0, "n": 3, "Key": "volumeFormula", "Value": volumeFormula}, + {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id}, + {"m": 0, "n": 3, "Key": "titrationType", "Value": titration_type} + ], + observe_id: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } + } + + self.pending_task_params.append(params) + print(f"成功添加液体进料小瓶(非滴定)参数: volume={volumeFormula}μL, material={assign_material_name}->ID:{material_id}") + print(f"当前队列长度: {len(self.pending_task_params)}") + return json.dumps({"suc": True}) + + def liquid_feeding_solvents(self, assign_material_name: str, volume: str, titration_type: str = "1", + time: str = "360", torque_variation: str = "2", temperature: float = 25.00): + """液体进料溶剂""" + self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_solvents"}') + material_id = self._get_material_id_by_name(assign_material_name) + + if isinstance(temperature, str): + temperature = float(temperature) + + liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["liquid"] + observe_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["observe"] + + params = { + "param_values": { + liquid_id: [ + {"m": 0, "n": 1, "Key": "titrationType", "Value": titration_type}, + {"m": 0, "n": 1, "Key": "volume", "Value": volume}, + {"m": 0, "n": 1, "Key": "assignMaterialName", "Value": material_id} + ], + observe_id: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } + } + + self.pending_task_params.append(params) + print(f"成功添加液体进料溶剂参数: material={assign_material_name}->ID:{material_id}, volume={volume}μL") + print(f"当前队列长度: {len(self.pending_task_params)}") + return json.dumps({"suc": True}) + + def liquid_feeding_titration(self, volume_formula: str, assign_material_name: str, titration_type: str = "1", + time: str = "90", torque_variation: int = 2, temperature: float = 25.00): + """液体进料(滴定)""" + self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}') + material_id = self._get_material_id_by_name(assign_material_name) + + if isinstance(temperature, str): + temperature = float(temperature) + + liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"] + observe_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"] + + params = { + "param_values": { + liquid_id: [ + {"m": 0, "n": 3, "Key": "volumeFormula", "Value": volume_formula}, + {"m": 0, "n": 3, "Key": "titrationType", "Value": titration_type}, + {"m": 0, "n": 3, "Key": "assignMaterialName", "Value": material_id} + ], + observe_id: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": str(torque_variation)}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } + } + + self.pending_task_params.append(params) + print(f"成功添加液体进料滴定参数: volume={volume_formula}μL, material={assign_material_name}->ID:{material_id}") + print(f"当前队列长度: {len(self.pending_task_params)}") + return json.dumps({"suc": True}) + + def liquid_feeding_beaker(self, volume: str = "35000", assign_material_name: str = "BAPP", + time: str = "0", torque_variation: str = "1", titrationType: str = "1", + temperature: float = 25.00): + """液体进料烧杯""" + self.append_to_workflow_sequence('{"web_workflow_name": "liquid_feeding_beaker"}') + material_id = self._get_material_id_by_name(assign_material_name) + + if isinstance(temperature, str): + temperature = float(temperature) + + liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["liquid"] + observe_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["observe"] + + params = { + "param_values": { + liquid_id: [ + {"m": 0, "n": 2, "Key": "volume", "Value": volume}, + {"m": 0, "n": 2, "Key": "assignMaterialName", "Value": material_id}, + {"m": 0, "n": 2, "Key": "titrationType", "Value": titrationType} + ], + observe_id: [ + {"m": 1, "n": 0, "Key": "time", "Value": time}, + {"m": 1, "n": 0, "Key": "torqueVariation", "Value": torque_variation}, + {"m": 1, "n": 0, "Key": "temperature", "Value": f"{temperature:.2f}"} + ] + } + } + + self.pending_task_params.append(params) + print(f"成功添加液体进料烧杯参数: volume={volume}μL, material={assign_material_name}->ID:{material_id}") + print(f"当前队列长度: {len(self.pending_task_params)}") + return json.dumps({"suc": True}) + + # 辅助方法 + def _load_material_cache(self): + """预加载材料列表到缓存中""" + try: + print("正在加载材料列表缓存...") + stock_query = '{"typeMode": 2, "includeDetail": true}' + stock_result = self.stock_material(stock_query) + + if isinstance(stock_result, str): + stock_data = json.loads(stock_result) + else: + stock_data = stock_result + + materials = stock_data + for material in materials: + material_name = material.get("name") + material_id = material.get("id") + if material_name and material_id: + self.material_cache[material_name] = material_id + + print(f"材料列表缓存加载完成,共加载 {len(self.material_cache)} 个材料") + + except Exception as e: + print(f"加载材料列表缓存时出错: {e}") + self.material_cache = {} + + def _get_material_id_by_name(self, material_name_or_id: str) -> str: + """根据材料名称获取材料ID""" + if len(material_name_or_id) > 20 and '-' in material_name_or_id: + return material_name_or_id + + if material_name_or_id in self.material_cache: + material_id = self.material_cache[material_name_or_id] + print(f"从缓存找到材料: {material_name_or_id} -> ID: {material_id}") + return material_id + + print(f"警告: 未在缓存中找到材料名称 '{material_name_or_id}',将使用原值") + return material_name_or_id + + def refresh_material_cache(self): + """刷新材料列表缓存""" + print("正在刷新材料列表缓存...") + self._load_material_cache() + + def get_available_materials(self): + """获取所有可用的材料名称列表""" + return list(self.material_cache.keys()) + + # 物料管理接口 + def add_material(self, json_str: str) -> dict: + """添加新的物料""" + try: + params = json.loads(json_str) + except: + return {} + + response = self.post( + url=f'{self.host}/api/lims/storage/material', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + def query_matial_type_id(self, data) -> list: + """查找物料typeid""" + response = self.post( + url=f'{self.host}/api/lims/storage/material-types', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": data + }) + + if not response or response['code'] != 1: + return [] + return str(response.get("data", {})) + + def query_warehouse_by_material_type(self, type_id: str) -> dict: + """查询物料类型可以入库的库位""" + params = {"typeId": type_id} + + response = self.post( + url=f'{self.host}/api/lims/storage/warehouse-info-by-mat-type-id', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + def material_inbound(self, material_id: str, location_name: str) -> dict: + """指定库位入库一个物料""" + location_id = LOCATION_MAPPING.get(location_name, location_name) + + params = { + "materialId": material_id, + "locationId": location_id + } + + response = self.post( + url=f'{self.host}/api/lims/storage/inbound', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + def delete_material(self, material_id: str) -> dict: + """删除尚未入库的物料""" + response = self.post( + url=f'{self.host}/api/lims/storage/delete-material', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": material_id + }) + + if not response or response['code'] != 1: + return {} + return response.get("data", {}) + + def material_outbound(self, material_id: str, location_name: str, quantity: int) -> dict: + """指定库位出库物料""" + location_id = LOCATION_MAPPING.get(location_name, location_name) + + params = { + "materialId": material_id, + "locationId": location_id, + "quantity": quantity + } + + response = self.post( + url=f'{self.host}/api/lims/storage/outbound', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response or response['code'] != 1: + return {} + return response + + def get_logger(self): + return self._logger diff --git a/unilabos/devices/workstation/bioyond_studio/experiment.py b/unilabos/devices/workstation/bioyond_studio/experiment.py new file mode 100644 index 00000000..ae3111b8 --- /dev/null +++ b/unilabos/devices/workstation/bioyond_studio/experiment.py @@ -0,0 +1,398 @@ +# experiment_workflow.py +""" +实验流程主程序 +""" + +import json +from bioyond_rpc import BioyondV1RPC +from config import API_CONFIG, WORKFLOW_MAPPINGS + + +def run_experiment(): + """运行实验流程""" + + # 初始化Bioyond客户端 + config = { + **API_CONFIG, + "workflow_mappings": WORKFLOW_MAPPINGS + } + + Bioyond = BioyondV1RPC(config) + + print("\n============= 多工作流参数测试(简化接口+材料缓存)=============") + + # 显示可用的材料名称(前20个) + available_materials = Bioyond.get_available_materials() + print(f"可用材料名称(前20个): {available_materials[:20]}") + print(f"总共有 {len(available_materials)} 个材料可用\n") + + # 1. 反应器放入 + print("1. 添加反应器放入工作流,带参数...") + Bioyond.reactor_taken_in( + assign_material_name="BTDA-DD", + cutoff="10000", + temperature="-10" + ) + + # 2. 液体投料-烧杯 (第一个) + print("2. 添加液体投料-烧杯,带参数...") + Bioyond.liquid_feeding_beaker( + volume="34768.7", + assign_material_name="ODA", + time="0", + torque_variation="1", + titrationType="1", + temperature=-10 + ) + + # 3. 液体投料-烧杯 (第二个) + print("3. 添加液体投料-烧杯,带参数...") + Bioyond.liquid_feeding_beaker( + volume="34080.9", + assign_material_name="MPDA", + time="5", + torque_variation="2", + titrationType="1", + temperature=0 + ) + + # 4. 液体投料-小瓶非滴定 + print("4. 添加液体投料-小瓶非滴定,带参数...") + Bioyond.liquid_feeding_vials_non_titration( + volumeFormula="639.5", + assign_material_name="SIDA", + titration_type="1", + time="0", + torque_variation="1", + temperature=-10 + ) + + # 5. 液体投料溶剂 + print("5. 添加液体投料溶剂,带参数...") + Bioyond.liquid_feeding_solvents( + assign_material_name="NMP", + volume="19000", + titration_type="1", + time="5", + torque_variation="2", + temperature=-10 + ) + + # 6-8. 固体进料小瓶 (三个) + print("6. 添加固体进料小瓶,带参数...") + Bioyond.solid_feeding_vials( + material_id="3", + time="180", + torque_variation="2", + assign_material_name="BTDA-1", + temperature=-10.00 + ) + + print("7. 添加固体进料小瓶,带参数...") + Bioyond.solid_feeding_vials( + material_id="3", + time="180", + torque_variation="2", + assign_material_name="BTDA-2", + temperature=25.00 + ) + + print("8. 添加固体进料小瓶,带参数...") + Bioyond.solid_feeding_vials( + material_id="3", + time="480", + torque_variation="2", + assign_material_name="BTDA-3", + temperature=25.00 + ) + + # 液体投料滴定(第一个) + print("9. 添加液体投料滴定,带参数...") # ODPA + Bioyond.liquid_feeding_titration( + volume_formula="1000", + assign_material_name="BTDA-DD", + titration_type="1", + time="360", + torque_variation="2", + temperature="25.00" + ) + + # 液体投料滴定(第二个) + print("10. 添加液体投料滴定,带参数...") # ODPA + Bioyond.liquid_feeding_titration( + volume_formula="500", + assign_material_name="BTDA-DD", + titration_type="1", + time="360", + torque_variation="2", + temperature="25.00" + ) + + # 液体投料滴定(第三个) + print("11. 添加液体投料滴定,带参数...") # ODPA + Bioyond.liquid_feeding_titration( + volume_formula="500", + assign_material_name="BTDA-DD", + titration_type="1", + time="360", + torque_variation="2", + temperature="25.00" + ) + + print("12. 添加液体投料滴定,带参数...") # ODPA + Bioyond.liquid_feeding_titration( + volume_formula="500", + assign_material_name="BTDA-DD", + titration_type="1", + time="360", + torque_variation="2", + temperature="25.00" + ) + + print("13. 添加液体投料滴定,带参数...") # ODPA + Bioyond.liquid_feeding_titration( + volume_formula="500", + assign_material_name="BTDA-DD", + titration_type="1", + time="360", + torque_variation="2", + temperature="25.00" + ) + + print("14. 添加液体投料滴定,带参数...") # ODPA + Bioyond.liquid_feeding_titration( + volume_formula="500", + assign_material_name="BTDA-DD", + titration_type="1", + time="360", + torque_variation="2", + temperature="25.00" + ) + + + + print("15. 添加液体投料溶剂,带参数...") + Bioyond.liquid_feeding_solvents( + assign_material_name="PGME", + volume="16894.6", + titration_type="1", + time="360", + torque_variation="2", + temperature=25.00 + ) + + # 16. 反应器取出 + print("16. 添加反应器取出工作流...") + Bioyond.reactor_taken_out() + + # 显示当前工作流序列 + sequence = Bioyond.get_workflow_sequence() + print("\n当前工作流执行顺序:") + print(sequence) + + # 执行process_and_execute_workflow,合并工作流并创建任务 + print("\n4. 执行process_and_execute_workflow...") + + result = Bioyond.process_and_execute_workflow( + workflow_name="test3_86", + task_name="实验3_86" + ) + + # 显示执行结果 + print("\n5. 执行结果:") + if isinstance(result, str): + try: + result_dict = json.loads(result) + if result_dict.get("success"): + print("任务创建成功!") + print(f"- 工作流: {result_dict.get('workflow', {}).get('name')}") + print(f"- 工作流ID: {result_dict.get('workflow', {}).get('id')}") + print(f"- 任务结果: {result_dict.get('task')}") + else: + print(f"任务创建失败: {result_dict.get('error')}") + except: + print(f"结果解析失败: {result}") + else: + if result.get("success"): + print("任务创建成功!") + print(f"- 工作流: {result.get('workflow', {}).get('name')}") + print(f"- 工作流ID: {result.get('workflow', {}).get('id')}") + print(f"- 任务结果: {result.get('task')}") + else: + print(f"任务创建失败: {result.get('error')}") + + # 可选:启动调度器 + # Bioyond.scheduler_start() + + return Bioyond + + +def prepare_materials(bioyond): + """准备实验材料(可选)""" + + # 样品板材料数据定义 + material_data_yp_1 = { + "typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e", + "name": "样品板-1", + "unit": "个", + "quantity": 1, + "details": [ + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BPDA-DD-1", + "quantity": 1, + "x": 1, + "y": 1, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "PEPA", + "quantity": 1, + "x": 1, + "y": 2, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BPDA-DD-2", + "quantity": 1, + "x": 1, + "y": 3, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BPDA-1", + "quantity": 1, + "x": 2, + "y": 1, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "PMDA", + "quantity": 1, + "x": 2, + "y": 2, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BPDA-2", + "quantity": 1, + "x": 2, + "y": 3, + "Parameters": "{\"molecular\": 1}" + } + ], + "Parameters": "{}" + } + + material_data_yp_2 = { + "typeId": "3a142339-80de-8f25-6093-1b1b1b6c322e", + "name": "样品板-2", + "unit": "个", + "quantity": 1, + "details": [ + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BPDA-DD", + "quantity": 1, + "x": 1, + "y": 1, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "SIDA", + "quantity": 1, + "x": 1, + "y": 2, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BTDA-1", + "quantity": 1, + "x": 2, + "y": 1, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BTDA-2", + "quantity": 1, + "x": 2, + "y": 2, + "Parameters": "{\"molecular\": 1}" + }, + { + "typeId": "3a14233a-84a3-088d-6676-7cb4acd57c64", + "name": "BTDA-3", + "quantity": 1, + "x": 2, + "y": 3, + "Parameters": "{\"molecular\": 1}" + } + ], + "Parameters": "{}" + } + + # 烧杯材料数据定义 + beaker_materials = [ + { + "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", + "name": "PDA-1", + "unit": "微升", + "quantity": 1, + "parameters": "{\"DeviceMaterialType\":\"NMP\"}" + }, + { + "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", + "name": "TFDB", + "unit": "微升", + "quantity": 1, + "parameters": "{\"DeviceMaterialType\":\"NMP\"}" + }, + { + "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", + "name": "ODA", + "unit": "微升", + "quantity": 1, + "parameters": "{\"DeviceMaterialType\":\"NMP\"}" + }, + { + "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", + "name": "MPDA", + "unit": "微升", + "quantity": 1, + "parameters": "{\"DeviceMaterialType\":\"NMP\"}" + }, + { + "typeId": "3a14233b-f0a9-ba84-eaa9-0d4718b361b6", + "name": "PDA-2", + "unit": "微升", + "quantity": 1, + "parameters": "{\"DeviceMaterialType\":\"NMP\"}" + } + ] + + # 如果需要,可以在这里调用add_material方法添加材料 + # 例如: + # result = bioyond.add_material(json.dumps(material_data_yp_1)) + # print(f"添加材料结果: {result}") + + return { + "sample_plates": [material_data_yp_1, material_data_yp_2], + "beakers": beaker_materials + } + + +if __name__ == "__main__": + # 运行主实验流程 + bioyond_client = run_experiment() + + # 可选:准备材料数据 + # materials = prepare_materials(bioyond_client) + # print(f"\n准备的材料数据: {materials}") diff --git a/unilabos/devices/workstation/bioyond_studio/station.py b/unilabos/devices/workstation/bioyond_studio/station.py new file mode 100644 index 00000000..6152a4eb --- /dev/null +++ b/unilabos/devices/workstation/bioyond_studio/station.py @@ -0,0 +1,334 @@ +""" +Bioyond工作站实现 +Bioyond Workstation Implementation + +集成Bioyond物料管理的工作站示例 +""" +from typing import Dict, Any, List, Optional, Union +import json + +from unilabos.devices.workstation.workstation_base import WorkstationBase, ResourceSynchronizer +from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker +from unilabos.utils.log import logger +from unilabos.resources.graphio import resource_bioyond_to_plr + + +class BioyondWorkstation(WorkstationBase): + """Bioyond工作站 + + 集成Bioyond物料管理的工作站实现 + """ + + def __init__( + self, + bioyond_config: Optional[Dict[str, Any]] = None, + deck: Optional[str, Any] = None, + *args, + **kwargs, + ): + # 设置Bioyond配置 + self.bioyond_config = bioyond_config or { + "base_url": "http://localhost:8080", + "api_key": "", + "sync_interval": 30, + "timeout": 30 + } + + # 设置默认deck配置 + + # 初始化父类 + super().__init__( + #桌子 + deck=deck, + *args, + **kwargs, + ) + + # TODO: self._ros_node里面拿属性 + logger.info(f"Bioyond工作站初始化完成") + + def _create_communication_module(self): + """创建Bioyond通信模块""" + # 暂时返回None,因为工作站基类没有强制要求通信模块 + return None + + def _create_material_management_module(self) -> BioyondMaterialManagement: + """创建Bioyond物料管理模块""" + # 获取必要的属性,如果不存在则使用默认值 + device_id = getattr(self, 'device_id', 'bioyond_workstation') + resource_tracker = getattr(self, 'resource_tracker', None) + children_config = getattr(self, '_children', {}) + + + + def _register_supported_workflows(self): + """注册Bioyond支持的工作流""" + from unilabos.devices.workstation.workstation_base import WorkflowInfo + + # Bioyond物料同步工作流 + self.supported_workflows["bioyond_sync"] = WorkflowInfo( + name="bioyond_sync", + description="从Bioyond系统同步物料", + parameters={ + "sync_type": {"type": "string", "default": "full", "options": ["full", "incremental"]}, + "force_sync": {"type": "boolean", "default": False} + } + ) + + # Bioyond物料更新工作流 + self.supported_workflows["bioyond_update"] = WorkflowInfo( + name="bioyond_update", + description="将本地物料变更同步到Bioyond", + parameters={ + "material_ids": {"type": "list", "default": []}, + "sync_all": {"type": "boolean", "default": True} + } + ) + + logger.info(f"注册了 {len(self.supported_workflows)} 个Bioyond工作流") + + async def execute_bioyond_sync_workflow(self, parameters: Dict[str, Any]) -> Dict[str, Any]: + """执行Bioyond同步工作流""" + try: + sync_type = parameters.get("sync_type", "full") + force_sync = parameters.get("force_sync", False) + + logger.info(f"开始执行Bioyond同步工作流: {sync_type}") + + # 获取物料管理模块 + material_manager = self.material_management + + if sync_type == "full": + # 全量同步 + success = await material_manager.sync_from_bioyond() + else: + # 增量同步(这里可以实现增量同步逻辑) + success = await material_manager.sync_from_bioyond() + + if success: + result = { + "status": "success", + "message": f"Bioyond同步完成: {sync_type}", + "synced_resources": len(material_manager.plr_resources) + } + else: + result = { + "status": "failed", + "message": "Bioyond同步失败" + } + + logger.info(f"Bioyond同步工作流执行完成: {result['status']}") + return result + + except Exception as e: + logger.error(f"Bioyond同步工作流执行失败: {e}") + return { + "status": "error", + "message": str(e) + } + + async def execute_bioyond_update_workflow(self, parameters: Dict[str, Any]) -> Dict[str, Any]: + """执行Bioyond更新工作流""" + try: + material_ids = parameters.get("material_ids", []) + sync_all = parameters.get("sync_all", True) + + logger.info(f"开始执行Bioyond更新工作流: sync_all={sync_all}") + + # 获取物料管理模块 + material_manager = self.material_management + + if sync_all: + # 同步所有物料 + success_count = 0 + for resource in material_manager.plr_resources.values(): + success = await material_manager.sync_to_bioyond(resource) + if success: + success_count += 1 + else: + # 同步指定物料 + success_count = 0 + for material_id in material_ids: + resource = material_manager.find_material_by_id(material_id) + if resource: + success = await material_manager.sync_to_bioyond(resource) + if success: + success_count += 1 + + result = { + "status": "success", + "message": f"Bioyond更新完成", + "updated_resources": success_count, + "total_resources": len(material_ids) if not sync_all else len(material_manager.plr_resources) + } + + logger.info(f"Bioyond更新工作流执行完成: {result['status']}") + return result + + except Exception as e: + logger.error(f"Bioyond更新工作流执行失败: {e}") + return { + "status": "error", + "message": str(e) + } + + def get_bioyond_status(self) -> Dict[str, Any]: + """获取Bioyond系统状态""" + try: + material_manager = self.material_management + + return { + "bioyond_connected": material_manager.bioyond_api_client is not None, + "sync_interval": material_manager.sync_interval, + "total_resources": len(material_manager.plr_resources), + "deck_size": { + "x": material_manager.plr_deck.size_x, + "y": material_manager.plr_deck.size_y, + "z": material_manager.plr_deck.size_z + }, + "bioyond_config": self.bioyond_config + } + except Exception as e: + logger.error(f"获取Bioyond状态失败: {e}") + return { + "error": str(e) + } + + def load_bioyond_data_from_file(self, file_path: str) -> bool: + """从文件加载Bioyond数据(用于测试)""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + bioyond_data = json.load(f) + + # 获取物料管理模块 + material_manager = self.material_management + + # 转换为UniLab格式 + if isinstance(bioyond_data, dict) and "data" in bioyond_data: + unilab_resources = material_manager.resource_bioyond_container_to_ulab(bioyond_data) + else: + unilab_resources = material_manager.resource_bioyond_to_ulab(bioyond_data) + + # 分配到Deck + import asyncio + asyncio.create_task(material_manager._assign_resources_to_deck(unilab_resources)) + + logger.info(f"从文件 {file_path} 加载了 {len(unilab_resources)} 个Bioyond资源") + return True + + except Exception as e: + logger.error(f"从文件加载Bioyond数据失败: {e}") + return False + + +# 使用示例 +def create_bioyond_workstation_example(): + """创建Bioyond工作站示例""" + + # 配置参数 + device_id = "bioyond_workstation_001" + + # 子资源配置 + children = { + "plate_1": { + "name": "plate_1", + "type": "plate", + "position": {"x": 100, "y": 100, "z": 0}, + "config": { + "size_x": 127.76, + "size_y": 85.48, + "size_z": 14.35, + "model": "Generic 96 Well Plate" + } + } + } + + # Bioyond配置 + bioyond_config = { + "base_url": "http://bioyond.example.com/api", + "api_key": "your_api_key_here", + "sync_interval": 60, # 60秒同步一次 + "timeout": 30 + } + + # Deck配置 + deck_config = { + "size_x": 1000.0, + "size_y": 1000.0, + "size_z": 100.0, + "model": "BioyondDeck" + } + + # 创建工作站 + workstation = BioyondWorkstation( + station_resource=deck_config, + bioyond_config=bioyond_config, + deck_config=deck_config, + ) + + return workstation + + +if __name__ == "__main__": + # 创建示例工作站 + #workstation = create_bioyond_workstation_example() + + # 从文件加载测试数据 + #workstation.load_bioyond_data_from_file("bioyond_test_yibin.json") + + # 获取状态 + #status = workstation.get_bioyond_status() + #print("Bioyond工作站状态:", status) + + # 创建测试数据 - 使用resource_bioyond_container_to_ulab函数期望的格式 + + # 读取 bioyond_resources_unilab_output3 copy.json 文件 + from unilabos.resources.graphio import resource_ulab_to_plr, convert_resources_to_type + from Bioyond_wuliao import * + from typing import List + from pylabrobot.resources import Resource as PLRResource + import json + from pylabrobot.resources.deck import Deck + from pylabrobot.resources.coordinate import Coordinate + + with open("./bioyond_test_yibin3_unilab_result_corr.json", "r", encoding="utf-8") as f: + bioyond_resources_unilab = json.load(f) + print(f"成功读取 JSON 文件,包含 {len(bioyond_resources_unilab)} 个资源") + ulab_resources = convert_resources_to_type(bioyond_resources_unilab, List[PLRResource]) + print(f"转换结果类型: {type(ulab_resources)}") + print(f"转换结果长度: {len(ulab_resources) if ulab_resources else 0}") + deck = Deck(size_x=2000, + size_y=653.5, + size_z=900) + + Stack0 = Stack(name="Stack0", location=Coordinate(0, 100, 0)) + Stack1 = Stack(name="Stack1", location=Coordinate(100, 100, 0)) + Stack2 = Stack(name="Stack2", location=Coordinate(200, 100, 0)) + Stack3 = Stack(name="Stack3", location=Coordinate(300, 100, 0)) + Stack4 = Stack(name="Stack4", location=Coordinate(400, 100, 0)) + Stack5 = Stack(name="Stack5", location=Coordinate(500, 100, 0)) + + deck.assign_child_resource(Stack1, Stack1.location) + deck.assign_child_resource(Stack2, Stack2.location) + deck.assign_child_resource(Stack3, Stack3.location) + deck.assign_child_resource(Stack4, Stack4.location) + deck.assign_child_resource(Stack5, Stack5.location) + + Stack0.assign_child_resource(ulab_resources[0], Stack0.location) + Stack1.assign_child_resource(ulab_resources[1], Stack1.location) + Stack2.assign_child_resource(ulab_resources[2], Stack2.location) + Stack3.assign_child_resource(ulab_resources[3], Stack3.location) + Stack4.assign_child_resource(ulab_resources[4], Stack4.location) + Stack5.assign_child_resource(ulab_resources[5], Stack5.location) + + from unilabos.resources.graphio import convert_resources_from_type + from unilabos.app.web.client import http_client + + resources = convert_resources_from_type([deck], [PLRResource]) + + + print(resources) + http_client.remote_addr = "https://uni-lab.bohrium.com/api/v1" + #http_client.auth = "9F05593C" + http_client.auth = "ED634D1C" + http_client.resource_add(resources, database_process_later=False) \ No newline at end of file diff --git a/unilabos/registry/devices/reaction_station_bioyong.yaml b/unilabos/registry/devices/reaction_station_bioyong.yaml new file mode 100644 index 00000000..f14e818e --- /dev/null +++ b/unilabos/registry/devices/reaction_station_bioyong.yaml @@ -0,0 +1,481 @@ +reaction_station_bioyong: + category: + - reaction_station_bioyong + class: + action_value_mappings: + drip_back: + feedback: {} + goal: + assign_material_name: assign_material_name + time: time + torque_variation: torque_variation + volume: volume + goal_default: + assign_material_name: '' + time: '' + torque_variation: '' + volume: '' + handles: {} + result: + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: ReactionStationDripBack_Feedback + type: object + goal: + properties: + assign_material_name: + type: string + time: + type: string + torque_variation: + type: string + volume: + type: string + required: + - volume + - assign_material_name + - time + - torque_variation + title: ReactionStationDripBack_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: ReactionStationDripBack_Result + type: object + required: + - goal + title: ReactionStationDripBack + type: object + type: ReactionStationDripBack + liquid_feeding_beaker: + feedback: {} + goal: + assign_material_name: assign_material_name + time: time + torque_variation: torque_variation + volume: volume + goal_default: + assign_material_name: '' + time: '' + titration_type: '' + torque_variation: '' + volume: '' + handles: {} + result: + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: ReactionStationLiquidFeed_Feedback + type: object + goal: + properties: + assign_material_name: + type: string + time: + type: string + titration_type: + type: string + torque_variation: + type: string + volume: + type: string + required: + - titration_type + - volume + - assign_material_name + - time + - torque_variation + title: ReactionStationLiquidFeed_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: ReactionStationLiquidFeed_Result + type: object + required: + - goal + title: ReactionStationLiquidFeed + type: object + type: ReactionStationLiquidFeed + liquid_feeding_solvents: + feedback: {} + goal: + assign_material_name: assign_material_name + time: time + torque_variation: torque_variation + volume: volume + goal_default: + assign_material_name: '' + time: '' + titration_type: '' + torque_variation: '' + volume: '' + handles: {} + result: + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: ReactionStationLiquidFeed_Feedback + type: object + goal: + properties: + assign_material_name: + type: string + time: + type: string + titration_type: + type: string + torque_variation: + type: string + volume: + type: string + required: + - titration_type + - volume + - assign_material_name + - time + - torque_variation + title: ReactionStationLiquidFeed_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: ReactionStationLiquidFeed_Result + type: object + required: + - goal + title: ReactionStationLiquidFeed + type: object + type: ReactionStationLiquidFeed + liquid_feeding_titration: + feedback: {} + goal: + assign_material_name: assign_material_name + time: time + torque_variation: torque_variation + volume: volume + goal_default: + assign_material_name: '' + time: '' + titration_type: '' + torque_variation: '' + volume: '' + handles: {} + result: + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: ReactionStationLiquidFeed_Feedback + type: object + goal: + properties: + assign_material_name: + type: string + time: + type: string + titration_type: + type: string + torque_variation: + type: string + volume: + type: string + required: + - titration_type + - volume + - assign_material_name + - time + - torque_variation + title: ReactionStationLiquidFeed_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: ReactionStationLiquidFeed_Result + type: object + required: + - goal + title: ReactionStationLiquidFeed + type: object + type: ReactionStationLiquidFeed + liquid_feeding_vials_non_titration: + feedback: {} + goal: + assign_material_name: assign_material_name + time: time + torque_variation: torque_variation + volume: volume + goal_default: + assign_material_name: '' + time: '' + titration_type: '' + torque_variation: '' + volume: '' + handles: {} + result: + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: ReactionStationLiquidFeed_Feedback + type: object + goal: + properties: + assign_material_name: + type: string + time: + type: string + titration_type: + type: string + torque_variation: + type: string + volume: + type: string + required: + - titration_type + - volume + - assign_material_name + - time + - torque_variation + title: ReactionStationLiquidFeed_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: ReactionStationLiquidFeed_Result + type: object + required: + - goal + title: ReactionStationLiquidFeed + type: object + type: ReactionStationLiquidFeed + process_and_execute_workflow: + feedback: {} + goal: + task_name: task_name + workflow_name: workflow_name + goal_default: + task_name: '' + workflow_name: '' + handles: {} + result: + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: ReactionStationProExecu_Feedback + type: object + goal: + properties: + task_name: + type: string + workflow_name: + type: string + required: + - workflow_name + - task_name + title: ReactionStationProExecu_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: ReactionStationProExecu_Result + type: object + required: + - goal + title: ReactionStationProExecu + type: object + type: ReactionStationProExecu + reactor_taken_in: + feedback: {} + goal: + assign_material_name: assign_material_name + cutoff: cutoff + temperature: temperature + goal_default: + assign_material_name: '' + cutoff: '' + temperature: '' + handles: {} + result: + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: ReactionStationReaTackIn_Feedback + type: object + goal: + properties: + assign_material_name: + type: string + cutoff: + type: string + temperature: + type: string + required: + - cutoff + - temperature + - assign_material_name + title: ReactionStationReaTackIn_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: ReactionStationReaTackIn_Result + type: object + required: + - goal + title: ReactionStationReaTackIn + type: object + type: ReactionStationReaTackIn + reactor_taken_out: + feedback: {} + goal: {} + goal_default: + command: '' + handles: {} + result: {} + schema: + description: '' + properties: + feedback: + properties: + status: + type: string + required: + - status + title: SendCmd_Feedback + type: object + goal: + properties: + command: + type: string + required: + - command + title: SendCmd_Goal + type: object + result: + properties: + return_info: + type: string + success: + type: boolean + required: + - return_info + - success + title: SendCmd_Result + type: object + required: + - goal + title: SendCmd + type: object + type: SendCmd + solid_feeding_vials: + feedback: {} + goal: + assign_material_name: assign_material_name + material_id: material_id + time: time + torque_variation: torque_variation + goal_default: + assign_material_name: '' + material_id: '' + time: '' + torque_variation: '' + handles: {} + result: + return_info: return_info + schema: + description: '' + properties: + feedback: + properties: {} + required: [] + title: ReactionStationSolidFeedVial_Feedback + type: object + goal: + properties: + assign_material_name: + type: string + material_id: + type: string + time: + type: string + torque_variation: + type: string + required: + - assign_material_name + - material_id + - time + - torque_variation + title: ReactionStationSolidFeedVial_Goal + type: object + result: + properties: + return_info: + type: string + required: + - return_info + title: ReactionStationSolidFeedVial_Result + type: object + required: + - goal + title: ReactionStationSolidFeedVial + type: object + type: ReactionStationSolidFeedVial + module: unilabos.devices.reaction_station.reaction_station_bioyong:BioyongV1RPC + status_types: {} + type: python + config_info: [] + description: reaction_station_bioyong Device + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 From a632fd495ecfc151b918aa43e3000e55e6c51166 Mon Sep 17 00:00:00 2001 From: Junhan Chang Date: Thu, 25 Sep 2025 20:56:29 +0800 Subject: [PATCH 09/17] bioyond station with communication init and resource sync --- .../workstation/bioyond_studio/__init__.py | 0 .../workstation/bioyond_studio/station.py | 114 ++++++++++++++---- .../devices/workstation/workstation_base.py | 61 ++-------- 3 files changed, 101 insertions(+), 74 deletions(-) create mode 100644 unilabos/devices/workstation/bioyond_studio/__init__.py diff --git a/unilabos/devices/workstation/bioyond_studio/__init__.py b/unilabos/devices/workstation/bioyond_studio/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/unilabos/devices/workstation/bioyond_studio/station.py b/unilabos/devices/workstation/bioyond_studio/station.py index 6152a4eb..bb701576 100644 --- a/unilabos/devices/workstation/bioyond_studio/station.py +++ b/unilabos/devices/workstation/bioyond_studio/station.py @@ -8,10 +8,89 @@ from typing import Dict, Any, List, Optional, Union import json from unilabos.devices.workstation.workstation_base import WorkstationBase, ResourceSynchronizer -from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker +from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondV1RPC from unilabos.utils.log import logger from unilabos.resources.graphio import resource_bioyond_to_plr +from .config import API_CONFIG, WORKFLOW_MAPPINGS + + +class BioyondResourceSynchronizer(ResourceSynchronizer): + """Bioyond资源同步器 + + 负责与Bioyond系统进行物料数据的同步 + """ + + def __init__(self, workstation: 'BioyondWorkstation'): + super().__init__(workstation) + self.bioyond_api_client = None + self.sync_interval = 60 # 默认60秒同步一次 + self.last_sync_time = 0 + + def initialize(self) -> bool: + """初始化Bioyond资源同步器""" + try: + self.bioyond_api_client = self.workstation.hardware_interface + if self.bioyond_api_client is None: + logger.error("Bioyond API客户端未初始化") + return False + + # 设置同步间隔 + self.sync_interval = self.workstation.bioyond_config.get("sync_interval", 60) + + logger.info("Bioyond资源同步器初始化完成") + return True + except Exception as e: + logger.error(f"Bioyond资源同步器初始化失败: {e}") + return False + + def sync_from_external(self) -> bool: + """从Bioyond系统同步物料数据""" + try: + if self.bioyond_api_client is None: + logger.error("Bioyond API客户端未初始化") + return False + + bioyond_data = self.bioyond_api_client.fetch_materials() + if not bioyond_data: + logger.warning("从Bioyond获取的物料数据为空") + return False + + # 转换为UniLab格式 + unilab_resources = resource_bioyond_to_plr(bioyond_data, deck=self.workstation.deck) + + logger.info(f"从Bioyond同步了 {len(unilab_resources)} 个资源") + return True + except Exception as e: + logger.error(f"从Bioyond同步物料数据失败: {e}") + return False + + def sync_to_external(self, resource: Any) -> bool: + """将本地物料数据变更同步到Bioyond系统""" + try: + if self.bioyond_api_client is None: + logger.error("Bioyond API客户端未初始化") + return False + + # 调用入库、出库操作 + # bioyond_format_data = self._convert_resource_to_bioyond_format(resource) + # success = await self.bioyond_api_client.update_material(bioyond_format_data) + # + # if success + except: + pass + + def handle_external_change(self, change_info: Dict[str, Any]) -> bool: + """处理Bioyond系统的变更通知""" + try: + # 这里可以实现对Bioyond变更的处理逻辑 + logger.info(f"处理Bioyond变更通知: {change_info}") + + return True + except Exception as e: + logger.error(f"处理Bioyond变更通知失败: {e}") + return False + class BioyondWorkstation(WorkstationBase): """Bioyond工作站 @@ -26,41 +105,30 @@ class BioyondWorkstation(WorkstationBase): *args, **kwargs, ): - # 设置Bioyond配置 - self.bioyond_config = bioyond_config or { - "base_url": "http://localhost:8080", - "api_key": "", - "sync_interval": 30, - "timeout": 30 - } - - # 设置默认deck配置 - + self._create_communication_module(bioyond_config) + # 初始化父类 super().__init__( - #桌子 + # 桌子 deck=deck, *args, **kwargs, ) + self.resource_synchronizer = BioyondResourceSynchronizer(self) + self.resource_synchronizer.sync_from_external() # TODO: self._ros_node里面拿属性 logger.info(f"Bioyond工作站初始化完成") - def _create_communication_module(self): + def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None: """创建Bioyond通信模块""" - # 暂时返回None,因为工作站基类没有强制要求通信模块 + self.bioyond_config = config or { + **API_CONFIG, + "workflow_mappings": WORKFLOW_MAPPINGS + } + self.hardware_interface = BioyondV1RPC(self.bioyond_config) return None - def _create_material_management_module(self) -> BioyondMaterialManagement: - """创建Bioyond物料管理模块""" - # 获取必要的属性,如果不存在则使用默认值 - device_id = getattr(self, 'device_id', 'bioyond_workstation') - resource_tracker = getattr(self, 'resource_tracker', None) - children_config = getattr(self, '_children', {}) - - - def _register_supported_workflows(self): """注册Bioyond支持的工作流""" from unilabos.devices.workstation.workstation_base import WorkflowInfo diff --git a/unilabos/devices/workstation/workstation_base.py b/unilabos/devices/workstation/workstation_base.py index 62b5e657..1988249f 100644 --- a/unilabos/devices/workstation/workstation_base.py +++ b/unilabos/devices/workstation/workstation_base.py @@ -112,17 +112,17 @@ class ResourceSynchronizer(ABC): self.workstation = workstation @abstractmethod - async def sync_from_external(self) -> bool: + def sync_from_external(self) -> bool: """从外部系统同步物料到本地deck""" pass @abstractmethod - async def sync_to_external(self, plr_resource: PLRResource) -> bool: + def sync_to_external(self, plr_resource: PLRResource) -> bool: """将本地物料同步到外部系统""" pass @abstractmethod - async def handle_external_change(self, change_info: Dict[str, Any]) -> bool: + def handle_external_change(self, change_info: Dict[str, Any]) -> bool: """处理外部系统的变更通知""" pass @@ -147,17 +147,15 @@ class WorkstationBase(ABC): def __init__( self, - deck: PLRResource, + deck: Deck, *args, **kwargs, # 必须有kwargs ): - # 基本配置 - print(deck) - self.deck_config = deck - # PLR 物料系统 - self.deck: Optional[Deck] = None + self.deck: Optional[Deck] = deck self.plr_resources: Dict[str, PLRResource] = {} + + self.resource_synchronizer = None # type: Optional[ResourceSynchronizer] # 硬件接口 self.hardware_interface: Union[Any, str] = None @@ -173,46 +171,7 @@ class WorkstationBase(ABC): def post_init(self, ros_node: ROS2WorkstationNode) -> None: # 初始化物料系统 self._ros_node = ros_node - self._initialize_material_system() - - def _initialize_material_system(self): - """初始化物料系统 - 使用 graphio 转换""" - pass - - def _create_complete_resource_config(self) -> Dict[str, Any]: - """创建完整的资源配置 - 合并 deck_config 和 children""" - # 创建主 deck 配置 - return {} - - def _normalize_child_resource(self, resource_id: str, config: Dict[str, Any], parent_id: str) -> Dict[str, Any]: - """标准化子资源配置""" - return { - "id": resource_id, - "name": config.get("name", resource_id), - "type": config.get("type", "container"), - "position": self._normalize_position(config.get("position", {})), - "config": config.get("config", {}), - "data": config.get("data", {}), - "children": [], # 简化版本:只支持一层子资源 - "parent": parent_id, - } - - def _normalize_position(self, position: Any) -> Dict[str, float]: - """标准化位置信息""" - if isinstance(position, dict): - return { - "x": float(position.get("x", 0)), - "y": float(position.get("y", 0)), - "z": float(position.get("z", 0)), - } - elif isinstance(position, (list, tuple)) and len(position) >= 2: - return { - "x": float(position[0]), - "y": float(position[1]), - "z": float(position[2]) if len(position) > 2 else 0.0, - } - else: - return {"x": 0.0, "y": 0.0, "z": 0.0} + self._ros_node.update_resource([self.deck]) def _build_resource_mappings(self, deck: Deck): """递归构建资源映射""" @@ -296,14 +255,14 @@ class WorkstationBase(ABC): """按类型查找资源""" return [res for res in self.plr_resources.values() if isinstance(res, resource_type)] - async def sync_with_external_system(self) -> bool: + def sync_with_external_system(self) -> bool: """与外部物料系统同步""" if not self.resource_synchronizer: logger.info(f"工作站 {self._ros_node.device_id} 没有配置资源同步器") return True try: - success = await self.resource_synchronizer.sync_from_external() + success = self.resource_synchronizer.sync_from_external() if success: logger.info(f"工作站 {self._ros_node.device_id} 外部同步成功") else: From 196e0f7e2bbd8630a13bc752480fd46fdfba19d2 Mon Sep 17 00:00:00 2001 From: Junhan Chang Date: Fri, 26 Sep 2025 08:12:41 +0800 Subject: [PATCH 10/17] fix bioyond station and registry --- .../reaction_station_bioyond_test.json | 67 +++ .../workstation/bioyond_studio/bioyond_rpc.py | 2 +- .../workstation/bioyond_studio/station.py | 26 +- .../devices/reaction_station_bioyong.yaml | 481 ------------------ unilabos/registry/devices/work_station.yaml | 98 ++++ .../resources/bioyond/bottle_carriers.yaml | 40 +- 6 files changed, 187 insertions(+), 527 deletions(-) create mode 100644 test/experiments/reaction_station_bioyond_test.json delete mode 100644 unilabos/registry/devices/reaction_station_bioyong.yaml diff --git a/test/experiments/reaction_station_bioyond_test.json b/test/experiments/reaction_station_bioyond_test.json new file mode 100644 index 00000000..9aeda04f --- /dev/null +++ b/test/experiments/reaction_station_bioyond_test.json @@ -0,0 +1,67 @@ +{ + "nodes": [ + { + "id": "reaction_station_bioyond", + "name": "reaction_station_bioyond", + "children": [ + ], + "parent": null, + "type": "device", + "class": "workstation.bioyond", + "config": { + "bioyond_config": { + "api_key": "DE9BDDA0", + "api_host": "http://192.168.1.200:44388", + "workflow_mappings": { + "reactor_taken_out": "3a16081e-4788-ca37-eff4-ceed8d7019d1", + "reactor_taken_in": "3a160df6-76b3-0957-9eb0-cb496d5721c6", + "Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6", + "Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47", + "Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046", + "Liquid_feeding(titration)": "3a160824-0665-01ed-285a-51ef817a9046", + "Liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784", + "Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a" + } + }, + "deck": { + "_resource_child_name": "Bioyond_Deck", + "_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck" + }, + "protocol_type": [] + }, + "data": {}, + "children": [ + "Bioyond_Deck" + ] + }, + { + "id": "Bioyond_Deck", + "name": "Bioyond_Deck", + "sample_id": null, + "children": [ + ], + "parent": "reaction_station_bioyond", + "type": "deck", + "class": "OTDeck", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "OTDeck", + "size_x": 624.3, + "size_y": 565.2, + "size_z": 900, + "with_trash": false, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + } + }, + "data": {} + } + ] +} \ No newline at end of file diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py b/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py index 9f46dfcf..f545a2ec 100644 --- a/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py +++ b/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py @@ -8,7 +8,7 @@ from datetime import datetime, timezone from unilabos.device_comms.rpc import BaseRequest from typing import Optional, List, Dict, Any import json -from config import WORKFLOW_TO_SECTION_MAP, WORKFLOW_STEP_IDS, LOCATION_MAPPING +from unilabos.devices.workstation.bioyond_studio.config import WORKFLOW_TO_SECTION_MAP, WORKFLOW_STEP_IDS, LOCATION_MAPPING class SimpleLogger: diff --git a/unilabos/devices/workstation/bioyond_studio/station.py b/unilabos/devices/workstation/bioyond_studio/station.py index bb701576..16ec6302 100644 --- a/unilabos/devices/workstation/bioyond_studio/station.py +++ b/unilabos/devices/workstation/bioyond_studio/station.py @@ -12,7 +12,7 @@ from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondV1RPC from unilabos.utils.log import logger from unilabos.resources.graphio import resource_bioyond_to_plr -from .config import API_CONFIG, WORKFLOW_MAPPINGS +from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG, WORKFLOW_MAPPINGS class BioyondResourceSynchronizer(ResourceSynchronizer): @@ -101,7 +101,7 @@ class BioyondWorkstation(WorkstationBase): def __init__( self, bioyond_config: Optional[Dict[str, Any]] = None, - deck: Optional[str, Any] = None, + deck: Optional[Any] = None, *args, **kwargs, ): @@ -240,28 +240,6 @@ class BioyondWorkstation(WorkstationBase): "message": str(e) } - def get_bioyond_status(self) -> Dict[str, Any]: - """获取Bioyond系统状态""" - try: - material_manager = self.material_management - - return { - "bioyond_connected": material_manager.bioyond_api_client is not None, - "sync_interval": material_manager.sync_interval, - "total_resources": len(material_manager.plr_resources), - "deck_size": { - "x": material_manager.plr_deck.size_x, - "y": material_manager.plr_deck.size_y, - "z": material_manager.plr_deck.size_z - }, - "bioyond_config": self.bioyond_config - } - except Exception as e: - logger.error(f"获取Bioyond状态失败: {e}") - return { - "error": str(e) - } - def load_bioyond_data_from_file(self, file_path: str) -> bool: """从文件加载Bioyond数据(用于测试)""" try: diff --git a/unilabos/registry/devices/reaction_station_bioyong.yaml b/unilabos/registry/devices/reaction_station_bioyong.yaml deleted file mode 100644 index f14e818e..00000000 --- a/unilabos/registry/devices/reaction_station_bioyong.yaml +++ /dev/null @@ -1,481 +0,0 @@ -reaction_station_bioyong: - category: - - reaction_station_bioyong - class: - action_value_mappings: - drip_back: - feedback: {} - goal: - assign_material_name: assign_material_name - time: time - torque_variation: torque_variation - volume: volume - goal_default: - assign_material_name: '' - time: '' - torque_variation: '' - volume: '' - handles: {} - result: - return_info: return_info - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: ReactionStationDripBack_Feedback - type: object - goal: - properties: - assign_material_name: - type: string - time: - type: string - torque_variation: - type: string - volume: - type: string - required: - - volume - - assign_material_name - - time - - torque_variation - title: ReactionStationDripBack_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: ReactionStationDripBack_Result - type: object - required: - - goal - title: ReactionStationDripBack - type: object - type: ReactionStationDripBack - liquid_feeding_beaker: - feedback: {} - goal: - assign_material_name: assign_material_name - time: time - torque_variation: torque_variation - volume: volume - goal_default: - assign_material_name: '' - time: '' - titration_type: '' - torque_variation: '' - volume: '' - handles: {} - result: - return_info: return_info - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: ReactionStationLiquidFeed_Feedback - type: object - goal: - properties: - assign_material_name: - type: string - time: - type: string - titration_type: - type: string - torque_variation: - type: string - volume: - type: string - required: - - titration_type - - volume - - assign_material_name - - time - - torque_variation - title: ReactionStationLiquidFeed_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: ReactionStationLiquidFeed_Result - type: object - required: - - goal - title: ReactionStationLiquidFeed - type: object - type: ReactionStationLiquidFeed - liquid_feeding_solvents: - feedback: {} - goal: - assign_material_name: assign_material_name - time: time - torque_variation: torque_variation - volume: volume - goal_default: - assign_material_name: '' - time: '' - titration_type: '' - torque_variation: '' - volume: '' - handles: {} - result: - return_info: return_info - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: ReactionStationLiquidFeed_Feedback - type: object - goal: - properties: - assign_material_name: - type: string - time: - type: string - titration_type: - type: string - torque_variation: - type: string - volume: - type: string - required: - - titration_type - - volume - - assign_material_name - - time - - torque_variation - title: ReactionStationLiquidFeed_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: ReactionStationLiquidFeed_Result - type: object - required: - - goal - title: ReactionStationLiquidFeed - type: object - type: ReactionStationLiquidFeed - liquid_feeding_titration: - feedback: {} - goal: - assign_material_name: assign_material_name - time: time - torque_variation: torque_variation - volume: volume - goal_default: - assign_material_name: '' - time: '' - titration_type: '' - torque_variation: '' - volume: '' - handles: {} - result: - return_info: return_info - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: ReactionStationLiquidFeed_Feedback - type: object - goal: - properties: - assign_material_name: - type: string - time: - type: string - titration_type: - type: string - torque_variation: - type: string - volume: - type: string - required: - - titration_type - - volume - - assign_material_name - - time - - torque_variation - title: ReactionStationLiquidFeed_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: ReactionStationLiquidFeed_Result - type: object - required: - - goal - title: ReactionStationLiquidFeed - type: object - type: ReactionStationLiquidFeed - liquid_feeding_vials_non_titration: - feedback: {} - goal: - assign_material_name: assign_material_name - time: time - torque_variation: torque_variation - volume: volume - goal_default: - assign_material_name: '' - time: '' - titration_type: '' - torque_variation: '' - volume: '' - handles: {} - result: - return_info: return_info - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: ReactionStationLiquidFeed_Feedback - type: object - goal: - properties: - assign_material_name: - type: string - time: - type: string - titration_type: - type: string - torque_variation: - type: string - volume: - type: string - required: - - titration_type - - volume - - assign_material_name - - time - - torque_variation - title: ReactionStationLiquidFeed_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: ReactionStationLiquidFeed_Result - type: object - required: - - goal - title: ReactionStationLiquidFeed - type: object - type: ReactionStationLiquidFeed - process_and_execute_workflow: - feedback: {} - goal: - task_name: task_name - workflow_name: workflow_name - goal_default: - task_name: '' - workflow_name: '' - handles: {} - result: - return_info: return_info - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: ReactionStationProExecu_Feedback - type: object - goal: - properties: - task_name: - type: string - workflow_name: - type: string - required: - - workflow_name - - task_name - title: ReactionStationProExecu_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: ReactionStationProExecu_Result - type: object - required: - - goal - title: ReactionStationProExecu - type: object - type: ReactionStationProExecu - reactor_taken_in: - feedback: {} - goal: - assign_material_name: assign_material_name - cutoff: cutoff - temperature: temperature - goal_default: - assign_material_name: '' - cutoff: '' - temperature: '' - handles: {} - result: - return_info: return_info - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: ReactionStationReaTackIn_Feedback - type: object - goal: - properties: - assign_material_name: - type: string - cutoff: - type: string - temperature: - type: string - required: - - cutoff - - temperature - - assign_material_name - title: ReactionStationReaTackIn_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: ReactionStationReaTackIn_Result - type: object - required: - - goal - title: ReactionStationReaTackIn - type: object - type: ReactionStationReaTackIn - reactor_taken_out: - feedback: {} - goal: {} - goal_default: - command: '' - handles: {} - result: {} - schema: - description: '' - properties: - feedback: - properties: - status: - type: string - required: - - status - title: SendCmd_Feedback - type: object - goal: - properties: - command: - type: string - required: - - command - title: SendCmd_Goal - type: object - result: - properties: - return_info: - type: string - success: - type: boolean - required: - - return_info - - success - title: SendCmd_Result - type: object - required: - - goal - title: SendCmd - type: object - type: SendCmd - solid_feeding_vials: - feedback: {} - goal: - assign_material_name: assign_material_name - material_id: material_id - time: time - torque_variation: torque_variation - goal_default: - assign_material_name: '' - material_id: '' - time: '' - torque_variation: '' - handles: {} - result: - return_info: return_info - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: ReactionStationSolidFeedVial_Feedback - type: object - goal: - properties: - assign_material_name: - type: string - material_id: - type: string - time: - type: string - torque_variation: - type: string - required: - - assign_material_name - - material_id - - time - - torque_variation - title: ReactionStationSolidFeedVial_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: ReactionStationSolidFeedVial_Result - type: object - required: - - goal - title: ReactionStationSolidFeedVial - type: object - type: ReactionStationSolidFeedVial - module: unilabos.devices.reaction_station.reaction_station_bioyong:BioyongV1RPC - status_types: {} - type: python - config_info: [] - description: reaction_station_bioyong Device - handles: [] - icon: '' - init_param_schema: {} - version: 1.0.0 diff --git a/unilabos/registry/devices/work_station.yaml b/unilabos/registry/devices/work_station.yaml index 74987ee0..48054810 100644 --- a/unilabos/registry/devices/work_station.yaml +++ b/unilabos/registry/devices/work_station.yaml @@ -6044,3 +6044,101 @@ workstation: required: [] type: object version: 1.0.0 +workstation.bioyond: + category: + - work_station + class: + action_value_mappings: + auto-execute_bioyond_sync_workflow: + feedback: {} + goal: {} + goal_default: + parameters: null + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + parameters: + type: object + required: + - parameters + type: object + result: {} + required: + - goal + title: execute_bioyond_sync_workflow参数 + type: object + type: UniLabJsonCommandAsync + auto-execute_bioyond_update_workflow: + feedback: {} + goal: {} + goal_default: + parameters: null + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + parameters: + type: object + required: + - parameters + type: object + result: {} + required: + - goal + title: execute_bioyond_update_workflow参数 + type: object + type: UniLabJsonCommandAsync + auto-load_bioyond_data_from_file: + feedback: {} + goal: {} + goal_default: + file_path: null + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + file_path: + type: string + required: + - file_path + type: object + result: {} + required: + - goal + title: load_bioyond_data_from_file参数 + type: object + type: UniLabJsonCommand + module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation + status_types: {} + type: python + config_info: [] + description: '' + handles: [] + icon: 反应站.webp + init_param_schema: + config: + properties: + bioyond_config: + type: string + deck: + type: string + required: [] + type: object + data: + properties: {} + required: [] + type: object + version: 1.0.0 diff --git a/unilabos/registry/resources/bioyond/bottle_carriers.yaml b/unilabos/registry/resources/bioyond/bottle_carriers.yaml index 221e6c74..0eaba1e3 100644 --- a/unilabos/registry/resources/bioyond/bottle_carriers.yaml +++ b/unilabos/registry/resources/bioyond/bottle_carriers.yaml @@ -1,38 +1,36 @@ -BIOYOND_PolymerStation_6VialCarrier: - category: - - bottle_carriers - class: - module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_6VialCarrier - type: pylabrobot - description: BIOYOND_PolymerStation_6VialCarrier - handles: [ ] - icon: '' - init_param_schema: { } - registry_type: resource - version: 1.0.0 - BIOYOND_PolymerStation_1BottleCarrier: category: - - bottle_carriers + - bottle_carriers class: module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1BottleCarrier type: pylabrobot description: BIOYOND_PolymerStation_1BottleCarrier - handles: [ ] + handles: [] icon: '' - init_param_schema: { } + init_param_schema: {} registry_type: resource version: 1.0.0 - BIOYOND_PolymerStation_1FlaskCarrier: category: - - bottle_carriers + - bottle_carriers class: module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_1FlaskCarrier type: pylabrobot description: BIOYOND_PolymerStation_1FlaskCarrier - handles: [ ] + handles: [] icon: '' - init_param_schema: { } + init_param_schema: {} registry_type: resource - version: 1.0.0 \ No newline at end of file + version: 1.0.0 +BIOYOND_PolymerStation_6VialCarrier: + category: + - bottle_carriers + class: + module: unilabos.resources.bioyond.bottle_carriers:BIOYOND_PolymerStation_6VialCarrier + type: pylabrobot + description: BIOYOND_PolymerStation_6VialCarrier + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 From a5397ffe120cf482cab4e3d65a947fbe1045f9d7 Mon Sep 17 00:00:00 2001 From: Junhan Chang Date: Fri, 26 Sep 2025 23:25:34 +0800 Subject: [PATCH 11/17] create/update resources with POST/PUT for big amount/ small amount data --- unilabos/app/web/client.py | 45 ++++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/unilabos/app/web/client.py b/unilabos/app/web/client.py index 61a98511..1420c448 100644 --- a/unilabos/app/web/client.py +++ b/unilabos/app/web/client.py @@ -25,6 +25,7 @@ class HTTPClient: remote_addr: 远程服务器地址,如果不提供则从配置中获取 auth: 授权信息 """ + self.initialized = False self.remote_addr = remote_addr or HTTPConfig.remote_addr if auth is not None: self.auth = auth @@ -69,12 +70,22 @@ class HTTPClient: Returns: Response: API响应对象 """ - response = requests.post( - f"{self.remote_addr}/lab/material", - json={"nodes": resources}, - headers={"Authorization": f"Lab {self.auth}"}, - timeout=100, - ) + if not self.initialized: + self.initialized = True + info(f"首次添加资源,当前远程地址: {self.remote_addr}") + response = requests.post( + f"{self.remote_addr}/lab/material", + json={"nodes": resources}, + headers={"Authorization": f"Lab {self.auth}"}, + timeout=100, + ) + else: + response = requests.put( + f"{self.remote_addr}/lab/material", + json={"nodes": resources}, + headers={"Authorization": f"Lab {self.auth}"}, + timeout=100, + ) if response.status_code == 200: res = response.json() if "code" in res and res["code"] != 0: @@ -130,12 +141,22 @@ class HTTPClient: Returns: Response: API响应对象 """ - response = requests.put( - f"{self.remote_addr}/lab/material", - json={"nodes": resources}, - headers={"Authorization": f"Lab {self.auth}"}, - timeout=100, - ) + if not self.initialized: + self.initialized = True + info(f"首次添加资源,当前远程地址: {self.remote_addr}") + response = requests.post( + f"{self.remote_addr}/lab/material", + json={"nodes": resources}, + headers={"Authorization": f"Lab {self.auth}"}, + timeout=100, + ) + else: + response = requests.put( + f"{self.remote_addr}/lab/material", + json={"nodes": resources}, + headers={"Authorization": f"Lab {self.auth}"}, + timeout=100, + ) if response.status_code == 200: res = response.json() if "code" in res and res["code"] != 0: From 10aabb75921ecda3ef973ecbed4cd7e7072f5429 Mon Sep 17 00:00:00 2001 From: Junhan Chang Date: Mon, 29 Sep 2025 20:36:45 +0800 Subject: [PATCH 12/17] refactor: add itemized_carrier instead of carrier consists of ResourceHolder --- unilabos/resources/bioyond/bottle_carriers.py | 68 ++-- unilabos/resources/bioyond/bottles.py | 2 +- unilabos/resources/bottle_carrier.py | 72 ---- unilabos/resources/graphio.py | 7 +- unilabos/resources/itemized_carrier.py | 322 ++++++++++++++++++ unilabos/resources/warehouse.py | 23 +- 6 files changed, 373 insertions(+), 121 deletions(-) delete mode 100644 unilabos/resources/bottle_carrier.py create mode 100644 unilabos/resources/itemized_carrier.py diff --git a/unilabos/resources/bioyond/bottle_carriers.py b/unilabos/resources/bioyond/bottle_carriers.py index cd84944b..9f88b88d 100644 --- a/unilabos/resources/bioyond/bottle_carriers.py +++ b/unilabos/resources/bioyond/bottle_carriers.py @@ -1,6 +1,6 @@ -from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder +from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d -from unilabos.resources.bottle_carrier import Bottle, BottleCarrier +from unilabos.resources.itemized_carrier import Bottle, BottleCarrier from unilabos.resources.bioyond.bottles import BIOYOND_PolymerStation_Solid_Vial, BIOYOND_PolymerStation_Solution_Beaker, BIOYOND_PolymerStation_Reagent_Bottle # 命名约定:试剂瓶-Bottle,烧杯-Beaker,烧瓶-Flask,小瓶-Vial @@ -22,27 +22,29 @@ def BIOYOND_Electrolyte_6VialCarrier(name: str) -> BottleCarrier: start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2 start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 - # 创建6个位置坐标 (2行 x 3列) - locations = [] - for row in range(2): - for col in range(3): - x = start_x + col * bottle_spacing_x - y = start_y + row * bottle_spacing_y - z = 5.0 # 架位底部 - locations.append(Coordinate(x, y, z)) + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=3, + num_items_y=2, + dx=start_x, + dy=start_y, + dz=5.0, + item_dx=bottle_spacing_x, + item_dy=bottle_spacing_y, + + size_x=bottle_diameter, + size_y=bottle_diameter, + size_z=carrier_size_z, + ) + for k, v in sites.items(): + v.name = f"{name}_{v.name}" carrier = BottleCarrier( name=name, size_x=carrier_size_x, size_y=carrier_size_y, size_z=carrier_size_z, - sites=create_homogeneous_resources( - klass=ResourceHolder, - locations=locations, - resource_size_x=bottle_diameter, - resource_size_y=bottle_diameter, - name_prefix=name, - ), + sites=sites, model="BIOYOND_Electrolyte_6VialCarrier", ) carrier.num_items_x = 3 @@ -107,27 +109,29 @@ def BIOYOND_PolymerStation_6VialCarrier(name: str) -> BottleCarrier: start_x = (carrier_size_x - (3 - 1) * bottle_spacing_x - bottle_diameter) / 2 start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 - # 创建6个位置坐标 (2行 x 3列) - locations = [] - for row in range(2): - for col in range(3): - x = start_x + col * bottle_spacing_x - y = start_y + row * bottle_spacing_y - z = 5.0 # 架位底部 - locations.append(Coordinate(x, y, z)) + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=3, + num_items_y=2, + dx=start_x, + dy=start_y, + dz=5.0, + item_dx=bottle_spacing_x, + item_dy=bottle_spacing_y, + + size_x=bottle_diameter, + size_y=bottle_diameter, + size_z=carrier_size_z, + ) + for k, v in sites.items(): + v.name = f"{name}_{v.name}" carrier = BottleCarrier( name=name, size_x=carrier_size_x, size_y=carrier_size_y, size_z=carrier_size_z, - sites=create_homogeneous_resources( - klass=ResourceHolder, - locations=locations, - resource_size_x=bottle_diameter, - resource_size_y=bottle_diameter, - name_prefix=name, - ), + sites=sites, model="BIOYOND_PolymerStation_6VialCarrier", ) carrier.num_items_x = 3 diff --git a/unilabos/resources/bioyond/bottles.py b/unilabos/resources/bioyond/bottles.py index b3725de9..1afac18c 100644 --- a/unilabos/resources/bioyond/bottles.py +++ b/unilabos/resources/bioyond/bottles.py @@ -1,4 +1,4 @@ -from unilabos.resources.bottle_carrier import Bottle, BottleCarrier +from unilabos.resources.itemized_carrier import Bottle, BottleCarrier # 工厂函数 diff --git a/unilabos/resources/bottle_carrier.py b/unilabos/resources/bottle_carrier.py deleted file mode 100644 index e680b26e..00000000 --- a/unilabos/resources/bottle_carrier.py +++ /dev/null @@ -1,72 +0,0 @@ -""" -自动化液体处理工作站物料类定义 - 简化版 -Automated Liquid Handling Station Resource Classes - Simplified Version -""" - -from __future__ import annotations - -from typing import Dict, Optional - -from pylabrobot.resources.coordinate import Coordinate -from pylabrobot.resources.container import Container -from pylabrobot.resources.carrier import TubeCarrier -from pylabrobot.resources.resource_holder import ResourceHolder - - -class Bottle(Container): - """瓶子类 - 简化版,不追踪瓶盖""" - - def __init__( - self, - name: str, - diameter: float, - height: float, - max_volume: float, - barcode: Optional[str] = "", - category: str = "container", - model: Optional[str] = None, - ): - super().__init__( - name=name, - size_x=diameter, - size_y=diameter, - size_z=height, - max_volume=max_volume, - category=category, - model=model, - ) - self.diameter = diameter - self.height = height - self.barcode = barcode - - def serialize(self) -> dict: - return { - **super().serialize(), - "diameter": self.diameter, - "height": self.height, - "barcode": self.barcode, - } - - -class BottleCarrier(TubeCarrier): - """瓶载架 - 直接继承自 TubeCarrier""" - - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - sites: Optional[Dict[int, ResourceHolder]] = None, - category: str = "bottle_carrier", - model: Optional[str] = None, - ): - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - sites=sites, - category=category, - model=model, - ) diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index 4bc611a2..0c55127a 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -4,6 +4,7 @@ import json from typing import Union, Any, Dict import numpy as np import networkx as nx +from pylabrobot.resources import ResourceHolder from unilabos_msgs.msg import Resource from unilabos.resources.container import RegularContainer @@ -507,7 +508,7 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict = number = (detail.get("z", 0) - 1) * plr_material.num_items_x * plr_material.num_items_y + \ (detail.get("x", 0) - 1) * plr_material.num_items_x + \ (detail.get("y", 0) - 1) - bottle = plr_material[number].resource + bottle = plr_material[number] bottle.code = detail.get("code", "") bottle.tracker.liquids = [(detail["name"], float(detail.get("quantity", 0)) if detail.get("quantity") else 0)] @@ -520,8 +521,8 @@ def resource_bioyond_to_plr(bioyond_materials: list[dict], type_mapping: dict = idx = (loc.get("y", 0) - 1) * warehouse.num_items_x * warehouse.num_items_y + \ (loc.get("x", 0) - 1) * warehouse.num_items_x + \ (loc.get("z", 0) - 1) - if 0 <= idx < warehouse.num_items_x * warehouse.num_items_y * warehouse.num_items_z: - if warehouse[idx].resource is None: + if 0 <= idx < warehouse.capacity: + if warehouse[idx] is None or isinstance(warehouse[idx], ResourceHolder): warehouse[idx] = plr_material return plr_materials diff --git a/unilabos/resources/itemized_carrier.py b/unilabos/resources/itemized_carrier.py new file mode 100644 index 00000000..77191307 --- /dev/null +++ b/unilabos/resources/itemized_carrier.py @@ -0,0 +1,322 @@ +""" +自动化液体处理工作站物料类定义 - 简化版 +Automated Liquid Handling Station Resource Classes - Simplified Version +""" + +from __future__ import annotations + +from typing import Dict, Optional + +from pylabrobot.resources.coordinate import Coordinate +from pylabrobot.resources.container import Container +from pylabrobot.resources.resource_holder import ResourceHolder +from pylabrobot.resources import Resource as ResourcePLR + + +class Bottle(Container): + """瓶子类 - 简化版,不追踪瓶盖""" + + def __init__( + self, + name: str, + diameter: float, + height: float, + max_volume: float, + size_x: float = 0.0, + size_y: float = 0.0, + size_z: float = 0.0, + barcode: Optional[str] = "", + category: str = "container", + model: Optional[str] = None, + ): + super().__init__( + name=name, + size_x=diameter, + size_y=diameter, + size_z=height, + max_volume=max_volume, + category=category, + model=model, + ) + self.diameter = diameter + self.height = height + self.barcode = barcode + + def serialize(self) -> dict: + return { + **super().serialize(), + "diameter": self.diameter, + "height": self.height, + "barcode": self.barcode, + } + + +from string import ascii_uppercase as LETTERS +from typing import Dict, List, Optional, Type, TypeVar, Union, Sequence, Tuple + +import pylabrobot +from pylabrobot.resources.resource_holder import ResourceHolder + +T = TypeVar("T", bound=ResourceHolder) + +S = TypeVar("S", bound=ResourceHolder) + + +class ItemizedCarrier(ResourcePLR): + """Base class for all carriers.""" + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None, + category: Optional[str] = "carrier", + model: Optional[str] = None, + ): + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + category=category, + model=model, + ) + sites = sites or {} + self.sites: List[Optional[ResourcePLR]] = list(sites.values()) + self._ordering = sites + self.num_items = len(self.sites) + self.child_locations: Dict[str, Coordinate] = {} + for spot, resource in sites.items(): + if resource is not None and getattr(resource, "location", None) is None: + raise ValueError(f"resource {resource} has no location") + if resource is not None: + self.child_locations[spot] = resource.location + else: + self.child_locations[spot] = Coordinate.zero() + + @property + def capacity(self): + """The number of sites on this carrier.""" + return len(self.sites) + + def __len__(self) -> int: + """Return the number of sites on this carrier.""" + return len(self.sites) + + def assign_child_resource( + self, + resource: ResourcePLR, + location: Optional[Coordinate], + reassign: bool = True, + spot: Optional[int] = None, + ): + idx = spot if spot is not None else len(self.sites) + if not reassign and self.sites[idx] is not None: + raise ValueError(f"a site with index {idx} already exists") + super().assign_child_resource(resource, location=location, reassign=reassign) + self.sites[idx] = resource + + def assign_resource_to_site(self, resource: ResourcePLR, spot: int): + if self.sites[spot] is not None and not isinstance(self.sites[spot], ResourceHolder): + raise ValueError(f"spot {spot} already has a resource, {resource}") + self.assign_child_resource(resource, location=self.child_locations.get(str(spot)), spot=spot) + + def unassign_child_resource(self, resource: ResourcePLR): + found = False + for spot, res in enumerate(self.sites): + if res == resource: + self.sites[spot] = None + found = True + break + if not found: + raise ValueError(f"Resource {resource} is not assigned to this carrier") + if hasattr(resource, "unassign"): + resource.unassign() + + def __getitem__( + self, + identifier: Union[str, int, Sequence[int], Sequence[str], slice, range], + ) -> Union[List[T], T]: + """Get the items with the given identifier. + + This is a convenience method for getting the items with the given identifier. It is equivalent + to :meth:`get_items`, but adds support for slicing and supports single items in the same + functional call. Note that the return type will always be a list, even if a single item is + requested. + + Examples: + Getting the items with identifiers "A1" through "E1": + + >>> items["A1:E1"] + + [, , , , ] + + Getting the items with identifiers 0 through 4 (note that this is the same as above): + + >>> items[range(5)] + + [, , , , ] + + Getting items with a slice (note that this is the same as above): + + >>> items[0:5] + + [, , , , ] + + Getting a single item: + + >>> items[0] + + [] + """ + + if isinstance(identifier, str): + if ":" in identifier: # multiple # TODO: deprecate this, use `"A1":"E1"` instead (slice) + return self.get_items(identifier) + + return self.get_item(identifier) # single + + if isinstance(identifier, int): + return self.get_item(identifier) + + if isinstance(identifier, (slice, range)): + start, stop = identifier.start, identifier.stop + if isinstance(identifier.start, str): + start = list(self._ordering.keys()).index(identifier.start) + elif identifier.start is None: + start = 0 + if isinstance(identifier.stop, str): + stop = list(self._ordering.keys()).index(identifier.stop) + elif identifier.stop is None: + stop = self.num_items + identifier = list(range(start, stop, identifier.step or 1)) + return self.get_items(identifier) + + if isinstance(identifier, (list, tuple)): + return self.get_items(identifier) + + raise TypeError(f"Invalid identifier type: {type(identifier)}") + + def get_item(self, identifier: Union[str, int, Tuple[int, int]]) -> T: + """Get the item with the given identifier. + + Args: + identifier: The identifier of the item. Either a string, an integer, or a tuple. If an + integer, it is the index of the item in the list of items (counted from 0, top to bottom, left + to right). If a string, it uses transposed MS Excel style notation, e.g. "A1" for the first + item, "B1" for the item below that, etc. If a tuple, it is (row, column). + + Raises: + IndexError: If the identifier is out of range. The range is 0 to self.num_items-1 (inclusive). + """ + + if isinstance(identifier, tuple): + row, column = identifier + identifier = LETTERS[row] + str(column + 1) # standard transposed-Excel style notation + if isinstance(identifier, str): + try: + identifier = list(self._ordering.keys()).index(identifier) + except ValueError as e: + raise IndexError( + f"Item with identifier '{identifier}' does not exist on " f"resource '{self.name}'." + ) from e + + if not 0 <= identifier < self.capacity: + raise IndexError( + f"Item with identifier '{identifier}' does not exist on " f"resource '{self.name}'." + ) + + # Cast child to item type. Children will always be `T`, but the type checker doesn't know that. + return self.sites[identifier] + + def get_items(self, identifiers: Union[str, Sequence[int], Sequence[str]]) -> List[T]: + """Get the items with the given identifier. + + Args: + identifier: Deprecated. Use `identifiers` instead. # TODO(deprecate-ordered-items) + identifiers: The identifiers of the items. Either a string range or a list of integers. If a + string, it uses transposed MS Excel style notation. Regions of items can be specified using + a colon, e.g. "A1:H1" for the first column. If a list of integers, it is the indices of the + items in the list of items (counted from 0, top to bottom, left to right). + + Examples: + Getting the items with identifiers "A1" through "E1": + + >>> items.get_items("A1:E1") + + [, , , , ] + + Getting the items with identifiers 0 through 4: + + >>> items.get_items(range(5)) + + [, , , , ] + """ + + if isinstance(identifiers, str): + identifiers = pylabrobot.utils.expand_string_range(identifiers) + return [self.get_item(i) for i in identifiers] + + def __setitem__(self, idx: Union[int, str], resource: Optional[ResourcePLR]): + """Assign a resource to this carrier.""" + if resource is None: # setting to None + assigned_resource = self[idx] + if assigned_resource is not None: + self.unassign_child_resource(assigned_resource) + else: + idx = list(self._ordering.keys()).index(idx) if isinstance(idx, str) else idx + self.assign_resource_to_site(resource, spot=idx) + + def __delitem__(self, idx: int): + """Unassign a resource from this carrier.""" + assigned_resource = self[idx] + if assigned_resource is not None: + self.unassign_child_resource(assigned_resource) + + def get_resources(self) -> List[ResourcePLR]: + """Get all resources assigned to this carrier.""" + return [resource for resource in self.sites.values() if resource is not None] + + def __eq__(self, other): + return super().__eq__(other) and self.sites == other.sites + + def get_free_sites(self) -> List[int]: + return [spot for spot, resource in self.sites.items() if resource is None] + + def serialize(self): + return { + **super().serialize(), + "slots": [{ + "label": str(identifier), + "visible": True if self[identifier] is not None else False, + "position": {"x": location.x, "y": location.y, "z": location.z}, + "size": {"width": self._size_x, "height": self._size_y, "depth": self._size_z}, + "content_type": ["bottle", "container", "tube", "bottle_carrier", "tip_rack"] + } for identifier, location in self.child_locations.items()] + } + + +class BottleCarrier(ItemizedCarrier): + """瓶载架 - 直接继承自 TubeCarrier""" + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + sites: Optional[Dict[Union[int, str], ResourceHolder]] = None, + category: str = "bottle_carrier", + model: Optional[str] = None, + ): + super().__init__( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + sites=sites, + category=category, + model=model, + ) diff --git a/unilabos/resources/warehouse.py b/unilabos/resources/warehouse.py index 11b945ed..87424b91 100644 --- a/unilabos/resources/warehouse.py +++ b/unilabos/resources/warehouse.py @@ -1,11 +1,11 @@ -import json from typing import Optional, List -from pylabrobot.resources import Coordinate, Resource -from pylabrobot.resources.carrier import Carrier, PlateHolder, ResourceHolder, create_homogeneous_resources -from pylabrobot.resources.deck import Deck +from pylabrobot.resources import Coordinate +from pylabrobot.resources.carrier import ResourceHolder, create_homogeneous_resources + +from unilabos.resources.itemized_carrier import ItemizedCarrier -class WareHouse(Carrier[ResourceHolder]): +class WareHouse(ItemizedCarrier): """4x4x1堆栈载体类 - 可容纳16个板位的载体(4层x4行x1列)""" def __init__( @@ -21,6 +21,7 @@ class WareHouse(Carrier[ResourceHolder]): item_dy: float = 10.0, item_dz: float = 10.0, removed_positions: Optional[List[int]] = None, + empty: bool = False, category: str = "warehouse", model: Optional[str] = None, ): @@ -44,13 +45,7 @@ class WareHouse(Carrier[ResourceHolder]): sites = create_homogeneous_resources( klass=ResourceHolder, - locations=[ - Coordinate(4.0, 8.5, 86.15), - Coordinate(4.0, 104.5, 86.15), - Coordinate(4.0, 200.5, 86.15), - Coordinate(4.0, 296.5, 86.15), - Coordinate(4.0, 392.5, 86.15), - ], + locations=locations, resource_size_x=127.0, resource_size_y=86.0, name_prefix=name, @@ -61,12 +56,14 @@ class WareHouse(Carrier[ResourceHolder]): size_x=dx + item_dx * num_items_x, size_y=dy + item_dy * num_items_y, size_z=dz + item_dz * num_items_z, + # ordered_items=ordered_items, + # ordering=ordering, sites=sites, category=category, model=model, ) - def get_site_by_layer_position(self, row: int, col: int, layer: int) -> PlateHolder: + def get_site_by_layer_position(self, row: int, col: int, layer: int) -> ResourceHolder: if not (0 <= layer < 4 and 0 <= row < 4 and 0 <= col < 1): raise ValueError("无效的位置: layer={}, row={}, col={}".format(layer, row, col)) From 7b426ed5aed27502124229ce9555c9be7cba5274 Mon Sep 17 00:00:00 2001 From: Junhan Chang Date: Tue, 30 Sep 2025 11:57:34 +0800 Subject: [PATCH 13/17] create warehouse by factory func --- .../workstation/bioyond_studio/station.py | 34 ++++-- unilabos/registry/resources/bioyond/deck.yaml | 12 ++ unilabos/resources/bioyond/decks.py | 17 ++- unilabos/resources/bioyond/warehouses.py | 8 +- unilabos/resources/itemized_carrier.py | 59 +++++++--- unilabos/resources/warehouse.py | 111 +++++++++++------- 6 files changed, 169 insertions(+), 72 deletions(-) create mode 100644 unilabos/registry/resources/bioyond/deck.yaml diff --git a/unilabos/devices/workstation/bioyond_studio/station.py b/unilabos/devices/workstation/bioyond_studio/station.py index 16ec6302..3685910a 100644 --- a/unilabos/devices/workstation/bioyond_studio/station.py +++ b/unilabos/devices/workstation/bioyond_studio/station.py @@ -4,15 +4,20 @@ Bioyond Workstation Implementation 集成Bioyond物料管理的工作站示例 """ +import traceback from typing import Dict, Any, List, Optional, Union import json from unilabos.devices.workstation.workstation_base import WorkstationBase, ResourceSynchronizer from unilabos.devices.workstation.bioyond_studio.bioyond_rpc import BioyondV1RPC +from unilabos.resources.warehouse import WareHouse from unilabos.utils.log import logger from unilabos.resources.graphio import resource_bioyond_to_plr -from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG, WORKFLOW_MAPPINGS +from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode +from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode + +from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS class BioyondResourceSynchronizer(ResourceSynchronizer): @@ -26,6 +31,7 @@ class BioyondResourceSynchronizer(ResourceSynchronizer): self.bioyond_api_client = None self.sync_interval = 60 # 默认60秒同步一次 self.last_sync_time = 0 + self.initialize() def initialize(self) -> bool: """初始化Bioyond资源同步器""" @@ -36,7 +42,7 @@ class BioyondResourceSynchronizer(ResourceSynchronizer): return False # 设置同步间隔 - self.sync_interval = self.workstation.bioyond_config.get("sync_interval", 60) + self.sync_interval = self.workstation.bioyond_config.get("sync_interval", 600) logger.info("Bioyond资源同步器初始化完成") return True @@ -51,18 +57,19 @@ class BioyondResourceSynchronizer(ResourceSynchronizer): logger.error("Bioyond API客户端未初始化") return False - bioyond_data = self.bioyond_api_client.fetch_materials() + bioyond_data = self.bioyond_api_client.stock_material('{"typeMode": 2, "includeDetail": true}') if not bioyond_data: logger.warning("从Bioyond获取的物料数据为空") return False # 转换为UniLab格式 - unilab_resources = resource_bioyond_to_plr(bioyond_data, deck=self.workstation.deck) + unilab_resources = resource_bioyond_to_plr(bioyond_data, type_mapping=self.workstation.bioyond_config["material_type_mappings"], deck=self.workstation.deck) logger.info(f"从Bioyond同步了 {len(unilab_resources)} 个资源") return True except Exception as e: logger.error(f"从Bioyond同步物料数据失败: {e}") + traceback.print_exc() return False def sync_to_external(self, resource: Any) -> bool: @@ -105,8 +112,6 @@ class BioyondWorkstation(WorkstationBase): *args, **kwargs, ): - self._create_communication_module(bioyond_config) - # 初始化父类 super().__init__( # 桌子 @@ -114,19 +119,34 @@ class BioyondWorkstation(WorkstationBase): *args, **kwargs, ) + self.deck.warehouses = {} + for resource in self.deck.children: + if isinstance(resource, WareHouse): + self.deck.warehouses[resource.name] = resource + + self._create_communication_module(bioyond_config) self.resource_synchronizer = BioyondResourceSynchronizer(self) self.resource_synchronizer.sync_from_external() # TODO: self._ros_node里面拿属性 logger.info(f"Bioyond工作站初始化完成") + + def post_init(self, ros_node: ROS2WorkstationNode): + self._ros_node = ros_node + #self.deck = create_a_coin_cell_deck() + ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ + "resources": [self.deck] + }) def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None: """创建Bioyond通信模块""" self.bioyond_config = config or { **API_CONFIG, - "workflow_mappings": WORKFLOW_MAPPINGS + "workflow_mappings": WORKFLOW_MAPPINGS, + "material_type_mappings": MATERIAL_TYPE_MAPPINGS } self.hardware_interface = BioyondV1RPC(self.bioyond_config) + return None def _register_supported_workflows(self): diff --git a/unilabos/registry/resources/bioyond/deck.yaml b/unilabos/registry/resources/bioyond/deck.yaml new file mode 100644 index 00000000..7026672b --- /dev/null +++ b/unilabos/registry/resources/bioyond/deck.yaml @@ -0,0 +1,12 @@ +BIOYOND_PolymerReactionStation_Deck: + category: + - deck + class: + module: unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck + type: pylabrobot + description: BIOYOND PolymerReactionStation Deck + handles: [] + icon: '' + init_param_schema: {} + registry_type: resource + version: 1.0.0 diff --git a/unilabos/resources/bioyond/decks.py b/unilabos/resources/bioyond/decks.py index d9fce97f..432ea171 100644 --- a/unilabos/resources/bioyond/decks.py +++ b/unilabos/resources/bioyond/decks.py @@ -1,12 +1,21 @@ -from pylabrobot.resources import Deck, Coordinate +from pylabrobot.resources import Deck, Coordinate, Rotation from unilabos.resources.bioyond.warehouses import bioyond_warehouse_1x4x4, bioyond_warehouse_1x4x2, bioyond_warehouse_liquid_and_lid_handling class BIOYOND_PolymerReactionStation_Deck(Deck): - def __init__(self, name: str = "PolymerReactionStation_Deck") -> None: + def __init__( + self, + name: str = "PolymerReactionStation_Deck", + size_x: float = 2700.0, + size_y: float = 1080.0, + size_z: float = 1500.0, + category: str = "deck", + setup: bool = False + ) -> None: super().__init__(name=name, size_x=2700.0, size_y=1080.0, size_z=1500.0) - self.setup() + if setup: + self.setup() def setup(self) -> None: # 添加仓库 @@ -20,7 +29,7 @@ class BIOYOND_PolymerReactionStation_Deck(Deck): "堆栈2": Coordinate(2550.0, 650.0, 0.0), "站内试剂存放堆栈": Coordinate(800.0, 475.0, 0.0), } - self.warehouses["站内试剂存放堆栈"].rotation = 90.0 + self.warehouses["站内试剂存放堆栈"].rotation = Rotation(z=90) for warehouse_name, warehouse in self.warehouses.items(): self.assign_child_resource(warehouse, location=self.warehouse_locations[warehouse_name]) diff --git a/unilabos/resources/bioyond/warehouses.py b/unilabos/resources/bioyond/warehouses.py index 9819a657..507b1f2f 100644 --- a/unilabos/resources/bioyond/warehouses.py +++ b/unilabos/resources/bioyond/warehouses.py @@ -1,9 +1,9 @@ -from unilabos.resources.warehouse import WareHouse +from unilabos.resources.warehouse import WareHouse, warehouse_factory def bioyond_warehouse_1x4x4(name: str) -> WareHouse: """创建BioYond 4x1x4仓库""" - return WareHouse( + return warehouse_factory( name=name, num_items_x=1, num_items_y=4, @@ -20,7 +20,7 @@ def bioyond_warehouse_1x4x4(name: str) -> WareHouse: def bioyond_warehouse_1x4x2(name: str) -> WareHouse: """创建BioYond 4x1x2仓库""" - return WareHouse( + return warehouse_factory( name=name, num_items_x=1, num_items_y=4, @@ -38,7 +38,7 @@ def bioyond_warehouse_1x4x2(name: str) -> WareHouse: def bioyond_warehouse_liquid_and_lid_handling(name: str) -> WareHouse: """创建BioYond开关盖加液模块台面""" - return WareHouse( + return warehouse_factory( name=name, num_items_x=2, num_items_y=5, diff --git a/unilabos/resources/itemized_carrier.py b/unilabos/resources/itemized_carrier.py index 77191307..61672c35 100644 --- a/unilabos/resources/itemized_carrier.py +++ b/unilabos/resources/itemized_carrier.py @@ -71,6 +71,9 @@ class ItemizedCarrier(ResourcePLR): size_x: float, size_y: float, size_z: float, + num_items_x: int = 0, + num_items_y: int = 0, + num_items_z: int = 0, sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None, category: Optional[str] = "carrier", model: Optional[str] = None, @@ -83,18 +86,27 @@ class ItemizedCarrier(ResourcePLR): category=category, model=model, ) - sites = sites or {} - self.sites: List[Optional[ResourcePLR]] = list(sites.values()) - self._ordering = sites - self.num_items = len(self.sites) - self.child_locations: Dict[str, Coordinate] = {} - for spot, resource in sites.items(): - if resource is not None and getattr(resource, "location", None) is None: - raise ValueError(f"resource {resource} has no location") - if resource is not None: - self.child_locations[spot] = resource.location - else: - self.child_locations[spot] = Coordinate.zero() + self.num_items = len(sites) + self.num_items_x, self.num_items_y, self.num_items_z = num_items_x, num_items_y, num_items_z + if isinstance(sites, dict): + sites = sites or {} + self.sites: List[Optional[ResourcePLR]] = list(sites.values()) + self._ordering = sites + self.child_locations: Dict[str, Coordinate] = {} + for spot, resource in sites.items(): + if resource is not None and getattr(resource, "location", None) is None: + raise ValueError(f"resource {resource} has no location") + if resource is not None: + self.child_locations[spot] = resource.location + else: + self.child_locations[spot] = Coordinate.zero() + elif isinstance(sites, list): + # deserialize时走这里;还需要根据 self.sites 索引children + self.child_locations = {site["label"]: Coordinate(**site["position"]) for site in sites} + self.sites = [site["occupied_by"] for site in sites] + self._ordering = {site["label"]: site["position"] for site in sites} + else: + print("sites:", sites) @property def capacity(self): @@ -112,7 +124,20 @@ class ItemizedCarrier(ResourcePLR): reassign: bool = True, spot: Optional[int] = None, ): - idx = spot if spot is not None else len(self.sites) + idx = spot + # 如果只给 location,根据坐标和 deserialize 后的 self.sites(持有names)来寻找 resource 该摆放的位置 + if spot is not None: + idx = spot + else: + for i, site in enumerate(self.sites): + site_location = list(self.child_locations.values())[i] + if type(site) == str and site == resource.name: + idx = i + break + if site_location == location: + idx = i + break + if not reassign and self.sites[idx] is not None: raise ValueError(f"a site with index {idx} already exists") super().assign_child_resource(resource, location=location, reassign=reassign) @@ -288,9 +313,15 @@ class ItemizedCarrier(ResourcePLR): def serialize(self): return { **super().serialize(), - "slots": [{ + "num_items_x": self.num_items_x, + "num_items_y": self.num_items_y, + "num_items_z": self.num_items_z, + "sites": [{ "label": str(identifier), "visible": True if self[identifier] is not None else False, + "occupied_by": self[identifier].name + if isinstance(self[identifier], ResourcePLR) and not isinstance(self[identifier], ResourceHolder) else + self[identifier] if isinstance(self[identifier], str) else None, "position": {"x": location.x, "y": location.y, "z": location.z}, "size": {"width": self._size_x, "height": self._size_y, "depth": self._size_z}, "content_type": ["bottle", "container", "tube", "bottle_carrier", "tip_rack"] diff --git a/unilabos/resources/warehouse.py b/unilabos/resources/warehouse.py index 87424b91..775c55aa 100644 --- a/unilabos/resources/warehouse.py +++ b/unilabos/resources/warehouse.py @@ -1,63 +1,88 @@ -from typing import Optional, List +from typing import Dict, Optional, List, Union from pylabrobot.resources import Coordinate from pylabrobot.resources.carrier import ResourceHolder, create_homogeneous_resources -from unilabos.resources.itemized_carrier import ItemizedCarrier +from unilabos.resources.itemized_carrier import ItemizedCarrier, ResourcePLR + + +def warehouse_factory( + name: str, + num_items_x: int = 4, + num_items_y: int = 1, + num_items_z: int = 4, + dx: float = 137.0, + dy: float = 96.0, + dz: float = 120.0, + item_dx: float = 10.0, + item_dy: float = 10.0, + item_dz: float = 10.0, + removed_positions: Optional[List[int]] = None, + empty: bool = False, + category: str = "warehouse", + model: Optional[str] = None, +): + # 创建16个板架位 (4层 x 4位置) + locations = [] + for layer in range(num_items_z): # 4层 + for row in range(num_items_y): # 4行 + for col in range(num_items_x): # 1列 (每层4x1=4个位置) + # 计算位置 + x = dx + col * item_dx + y = dy + (num_items_y - row - 1) * item_dy + z = dz + (num_items_z - layer - 1) * item_dz + locations.append(Coordinate(x, y, z)) + if removed_positions: + locations = [loc for i, loc in enumerate(locations) if i not in removed_positions] + sites = create_homogeneous_resources( + klass=ResourceHolder, + locations=locations, + resource_size_x=127.0, + resource_size_y=86.0, + name_prefix=name, + ) + + return WareHouse( + name=name, + size_x=dx + item_dx * num_items_x, + size_y=dy + item_dy * num_items_y, + size_z=dz + item_dz * num_items_z, + num_items_x = num_items_x, + num_items_y = num_items_y, + num_items_z = num_items_z, + # ordered_items=ordered_items, + # ordering=ordering, + sites=sites, + category=category, + model=model, + ) class WareHouse(ItemizedCarrier): - """4x4x1堆栈载体类 - 可容纳16个板位的载体(4层x4行x1列)""" - + """堆栈载体类 - 可容纳16个板位的载体(4层x4行x1列)""" def __init__( self, name: str, - num_items_x: int = 4, - num_items_y: int = 1, - num_items_z: int = 4, - dx: float = 137.0, - dy: float = 96.0, - dz: float = 120.0, - item_dx: float = 10.0, - item_dy: float = 10.0, - item_dz: float = 10.0, - removed_positions: Optional[List[int]] = None, - empty: bool = False, + size_x: float, + size_y: float, + size_z: float, + num_items_x: int, + num_items_y: int, + num_items_z: int, + sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None, category: str = "warehouse", model: Optional[str] = None, ): - self.num_items_x = num_items_x - self.num_items_y = num_items_y - self.num_items_z = num_items_z - # 创建16个板架位 (4层 x 4位置) - locations = [] - - for layer in range(num_items_z): # 4层 - for row in range(num_items_y): # 4行 - for col in range(num_items_x): # 1列 (每层4x1=4个位置) - # 计算位置 - x = dx + col * item_dx - y = dy + (num_items_y - row - 1) * item_dy - z = dz + (num_items_z - layer - 1) * item_dz - - locations.append(Coordinate(x, y, z)) - if removed_positions: - locations = [loc for i, loc in enumerate(locations) if i not in removed_positions] - - sites = create_homogeneous_resources( - klass=ResourceHolder, - locations=locations, - resource_size_x=127.0, - resource_size_y=86.0, - name_prefix=name, - ) super().__init__( name=name, - size_x=dx + item_dx * num_items_x, - size_y=dy + item_dy * num_items_y, - size_z=dz + item_dz * num_items_z, + size_x=size_x, + size_y=size_y, + size_z=size_z, # ordered_items=ordered_items, # ordering=ordering, + num_items_x=num_items_x, + num_items_y=num_items_y, + num_items_z=num_items_z, sites=sites, category=category, model=model, From 2476821dcc0a26617ecc0844e931711f959c4721 Mon Sep 17 00:00:00 2001 From: Junhan Chang Date: Tue, 30 Sep 2025 12:25:21 +0800 Subject: [PATCH 14/17] update bioyond launch json --- .../reaction_station_bioyond_test.json | 30 ++++++++++--------- unilabos/registry/resources/bioyond/deck.yaml | 2 +- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/test/experiments/reaction_station_bioyond_test.json b/test/experiments/reaction_station_bioyond_test.json index 9aeda04f..8446373a 100644 --- a/test/experiments/reaction_station_bioyond_test.json +++ b/test/experiments/reaction_station_bioyond_test.json @@ -3,9 +3,10 @@ { "id": "reaction_station_bioyond", "name": "reaction_station_bioyond", - "children": [ - ], "parent": null, + "children": [ + "Bioyond_Deck" + ], "type": "device", "class": "workstation.bioyond", "config": { @@ -21,18 +22,22 @@ "Liquid_feeding(titration)": "3a160824-0665-01ed-285a-51ef817a9046", "Liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784", "Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a" + }, + "material_type_mappings": { + "烧杯": "BIOYOND_PolymerStation_1FlaskCarrier", + "试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier", + "样品板": "BIOYOND_PolymerStation_6VialCarrier" } }, "deck": { - "_resource_child_name": "Bioyond_Deck", - "_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck" + "data": { + "_resource_child_name": "Bioyond_Deck", + "_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck" + } }, "protocol_type": [] }, - "data": {}, - "children": [ - "Bioyond_Deck" - ] + "data": {} }, { "id": "Bioyond_Deck", @@ -42,18 +47,15 @@ ], "parent": "reaction_station_bioyond", "type": "deck", - "class": "OTDeck", + "class": "BIOYOND_PolymerReactionStation_Deck", "position": { "x": 0, "y": 0, "z": 0 }, "config": { - "type": "OTDeck", - "size_x": 624.3, - "size_y": 565.2, - "size_z": 900, - "with_trash": false, + "type": "BIOYOND_PolymerReactionStation_Deck", + "setup": true, "rotation": { "x": 0, "y": 0, diff --git a/unilabos/registry/resources/bioyond/deck.yaml b/unilabos/registry/resources/bioyond/deck.yaml index 7026672b..c0ec18b6 100644 --- a/unilabos/registry/resources/bioyond/deck.yaml +++ b/unilabos/registry/resources/bioyond/deck.yaml @@ -6,7 +6,7 @@ BIOYOND_PolymerReactionStation_Deck: type: pylabrobot description: BIOYOND PolymerReactionStation Deck handles: [] - icon: '' + icon: '反应站.webp' init_param_schema: {} registry_type: resource version: 1.0.0 From 3fe8f4ca44754c814f83621c2ab68ae57fab532d Mon Sep 17 00:00:00 2001 From: Junhan Chang Date: Tue, 30 Sep 2025 12:58:42 +0800 Subject: [PATCH 15/17] add child_size for itemized_carrier --- unilabos/resources/bioyond/decks.py | 4 ++-- unilabos/resources/itemized_carrier.py | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/unilabos/resources/bioyond/decks.py b/unilabos/resources/bioyond/decks.py index 432ea171..923bf04b 100644 --- a/unilabos/resources/bioyond/decks.py +++ b/unilabos/resources/bioyond/decks.py @@ -25,8 +25,8 @@ class BIOYOND_PolymerReactionStation_Deck(Deck): "站内试剂存放堆栈": bioyond_warehouse_liquid_and_lid_handling("站内试剂存放堆栈"), } self.warehouse_locations = { - "堆栈1": Coordinate(0.0, 650.0, 0.0), - "堆栈2": Coordinate(2550.0, 650.0, 0.0), + "堆栈1": Coordinate(0.0, 430.0, 0.0), + "堆栈2": Coordinate(2550.0, 430.0, 0.0), "站内试剂存放堆栈": Coordinate(800.0, 475.0, 0.0), } self.warehouses["站内试剂存放堆栈"].rotation = Rotation(z=90) diff --git a/unilabos/resources/itemized_carrier.py b/unilabos/resources/itemized_carrier.py index 61672c35..3205c9dd 100644 --- a/unilabos/resources/itemized_carrier.py +++ b/unilabos/resources/itemized_carrier.py @@ -93,16 +93,20 @@ class ItemizedCarrier(ResourcePLR): self.sites: List[Optional[ResourcePLR]] = list(sites.values()) self._ordering = sites self.child_locations: Dict[str, Coordinate] = {} + self.child_size: Dict[str, dict] = {} for spot, resource in sites.items(): if resource is not None and getattr(resource, "location", None) is None: raise ValueError(f"resource {resource} has no location") if resource is not None: self.child_locations[spot] = resource.location + self.child_size[spot] = {"width": resource.size_x, "height": resource.size_y, "depth": resource.size_z} else: self.child_locations[spot] = Coordinate.zero() + self.child_size[spot] = {"width": 0, "height": 0, "depth": 0} elif isinstance(sites, list): # deserialize时走这里;还需要根据 self.sites 索引children self.child_locations = {site["label"]: Coordinate(**site["position"]) for site in sites} + self.child_size = {site["label"]: site["size"] for site in sites} self.sites = [site["occupied_by"] for site in sites] self._ordering = {site["label"]: site["position"] for site in sites} else: @@ -323,7 +327,7 @@ class ItemizedCarrier(ResourcePLR): if isinstance(self[identifier], ResourcePLR) and not isinstance(self[identifier], ResourceHolder) else self[identifier] if isinstance(self[identifier], str) else None, "position": {"x": location.x, "y": location.y, "z": location.z}, - "size": {"width": self._size_x, "height": self._size_y, "depth": self._size_z}, + "size": self.child_size[identifier], "content_type": ["bottle", "container", "tube", "bottle_carrier", "tip_rack"] } for identifier, location in self.child_locations.items()] } From 1c9d2ee98af82c11ced8b4a83ea34ff806ee6260 Mon Sep 17 00:00:00 2001 From: Junhan Chang Date: Tue, 30 Sep 2025 17:02:38 +0800 Subject: [PATCH 16/17] fix bioyond resource io --- .../workstation/workstation_http_service.py | 149 +++++++++++++++--- unilabos/registry/resources/bioyond/deck.yaml | 12 ++ unilabos/resources/bioyond/decks.py | 13 +- unilabos/resources/itemized_carrier.py | 2 +- 4 files changed, 152 insertions(+), 24 deletions(-) diff --git a/unilabos/devices/workstation/workstation_http_service.py b/unilabos/devices/workstation/workstation_http_service.py index 3805d2ce..4565edea 100644 --- a/unilabos/devices/workstation/workstation_http_service.py +++ b/unilabos/devices/workstation/workstation_http_service.py @@ -149,6 +149,22 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler): ) self._send_response(error_response) + def do_OPTIONS(self): + """处理OPTIONS请求 - CORS预检请求""" + try: + # 发送CORS响应头 + self.send_response(200) + self.send_header('Access-Control-Allow-Origin', '*') + self.send_header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS') + self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization') + self.send_header('Access-Control-Max-Age', '86400') + self.end_headers() + + except Exception as e: + logger.error(f"OPTIONS请求处理失败: {e}") + self.send_response(500) + self.end_headers() + def _handle_step_finish_report(self, request_data: Dict[str, Any]) -> HttpResponse: """处理步骤完成报送(统一LIMS协议规范)""" try: @@ -206,7 +222,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler): # 验证data字段内容 data = request_data['data'] - data_required_fields = ['orderCode', 'orderName', 'sampleId', 'startTime', 'endTime', 'Status'] + data_required_fields = ['orderCode', 'orderName', 'sampleId', 'startTime', 'endTime', 'status'] if data_missing_fields := [field for field in data_required_fields if field not in data]: return HttpResponse( success=False, @@ -227,7 +243,7 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler): "0": "待生产", "2": "进样", "10": "开始", "20": "完成", "-2": "异常停止", "-3": "人工停止" } - status_desc = status_names.get(str(data['Status']), f"状态{data['Status']}") + status_desc = status_names.get(str(data['status']), f"状态{data['status']}") return HttpResponse( success=True, @@ -380,6 +396,21 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler): """处理物料变更报送""" try: # 验证必需字段 + if 'brand' in request_data: + if request_data['brand'] == "bioyond": # 奔曜 + error_msg = request_data["text"] + logger.info(f"收到奔曜错误处理报送: {error_msg}") + return HttpResponse( + success=True, + message=f"错误处理报送已收到: {error_msg}", + acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{error_msg.get('action_id', 'unknown')}", + data=None + ) + else: + return HttpResponse( + success=False, + message=f"缺少厂家信息(brand字段)" + ) required_fields = ['workstation_id', 'timestamp', 'resource_id', 'change_type'] if missing_fields := [field for field in required_fields if field not in request_data]: return HttpResponse( @@ -407,23 +438,45 @@ class WorkstationHTTPHandler(BaseHTTPRequestHandler): def _handle_error_handling_report(self, request_data: Dict[str, Any]) -> HttpResponse: """处理错误处理报送""" try: - # 验证必需字段 - required_fields = ['workstation_id', 'timestamp', 'error_type', 'error_message'] - if missing_fields := [field for field in required_fields if field not in request_data]: + # 检查是否为奔曜格式的错误报送 + if 'brand' in request_data and str(request_data['brand']).lower() == "bioyond": + # 奔曜格式处理 + if 'text' not in request_data: + return HttpResponse( + success=False, + message="奔曜格式缺少text字段" + ) + + error_data = request_data["text"] + logger.info(f"收到奔曜错误处理报送: {error_data}") + + # 调用工作站的处理方法 + result = self.workstation.handle_external_error(error_data) + return HttpResponse( - success=False, - message=f"缺少必要字段: {', '.join(missing_fields)}" + success=True, + message=f"错误处理报送已收到: 任务{error_data.get('task', 'unknown')}, 错误代码{error_data.get('code', 'unknown')}", + acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{error_data.get('task', 'unknown')}", + data=result + ) + else: + # 标准格式处理 + required_fields = ['workstation_id', 'timestamp', 'error_type', 'error_message'] + if missing_fields := [field for field in required_fields if field not in request_data]: + return HttpResponse( + success=False, + message=f"缺少必要字段: {', '.join(missing_fields)}" + ) + + # 调用工作站的处理方法 + result = self.workstation.handle_external_error(request_data) + + return HttpResponse( + success=True, + message=f"错误处理报送已处理: {request_data['error_type']} - {request_data['error_message']}", + acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{request_data.get('action_id', 'unknown')}", + data=result ) - - # 调用工作站的处理方法 - result = self.workstation.handle_external_error(request_data) - - return HttpResponse( - success=True, - message=f"错误处理报送已处理: {request_data['error_type']} - {request_data['error_message']}", - acknowledgment_id=f"ERROR_{int(time.time() * 1000)}_{request_data.get('action_id', 'unknown')}", - data=result - ) except Exception as e: logger.error(f"处理错误处理报送失败: {e}") @@ -548,13 +601,19 @@ class WorkstationHTTPService: """停止HTTP服务""" try: if self.running and self.server: + logger.info("正在停止工作站HTTP报送服务...") self.running = False - self.server.shutdown() - self.server.server_close() + # 停止serve_forever循环 + self.server.shutdown() + + # 等待服务器线程结束 if self.server_thread and self.server_thread.is_alive(): self.server_thread.join(timeout=5.0) + # 关闭服务器套接字 + self.server.server_close() + logger.info("工作站HTTP报送服务已停止") except Exception as e: @@ -563,11 +622,13 @@ class WorkstationHTTPService: def _run_server(self): """运行HTTP服务器""" try: - while self.running: - self.server.handle_request() + # 使用serve_forever()让服务持续运行 + self.server.serve_forever() except Exception as e: if self.running: # 只在非正常停止时记录错误 logger.error(f"HTTP服务运行错误: {e}") + finally: + logger.info("HTTP服务器线程已退出") @property def is_running(self) -> bool: @@ -603,3 +664,49 @@ __all__ = [ 'MaterialChangeReport', 'TaskExecutionReport' ] + + +if __name__ == "__main__": + # 简单测试HTTP服务 + class DummyWorkstation: + device_id = "WS-001" + + def process_step_finish_report(self, report_request): + return {"processed": True} + + def process_sample_finish_report(self, report_request): + return {"processed": True} + + def process_order_finish_report(self, report_request, used_materials): + return {"processed": True} + + def process_material_change_report(self, report_data): + return {"processed": True} + + def handle_external_error(self, error_data): + return {"handled": True} + + workstation = DummyWorkstation() + http_service = WorkstationHTTPService(workstation) + + try: + http_service.start() + print(f"测试服务器已启动: {http_service.service_url}") + print("按 Ctrl+C 停止服务器") + print("服务将持续运行,等待接收HTTP请求...") + + # 保持服务器运行 - 使用更好的等待机制 + try: + while http_service.is_running: + time.sleep(1) + except KeyboardInterrupt: + print("\n接收到停止信号...") + + except KeyboardInterrupt: + print("\n正在停止服务器...") + http_service.stop() + print("服务器已停止") + except Exception as e: + print(f"服务器运行错误: {e}") + http_service.stop() + diff --git a/unilabos/registry/resources/bioyond/deck.yaml b/unilabos/registry/resources/bioyond/deck.yaml index c0ec18b6..cb655126 100644 --- a/unilabos/registry/resources/bioyond/deck.yaml +++ b/unilabos/registry/resources/bioyond/deck.yaml @@ -10,3 +10,15 @@ BIOYOND_PolymerReactionStation_Deck: init_param_schema: {} registry_type: resource version: 1.0.0 +BIOYOND_PolymerPreparationStation_Deck: + category: + - deck + class: + module: unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck + type: pylabrobot + description: BIOYOND PolymerPreparationStation Deck + handles: [] + icon: '配液站.webp' + init_param_schema: {} + registry_type: resource + version: 1.0.0 diff --git a/unilabos/resources/bioyond/decks.py b/unilabos/resources/bioyond/decks.py index 923bf04b..e8c021fd 100644 --- a/unilabos/resources/bioyond/decks.py +++ b/unilabos/resources/bioyond/decks.py @@ -36,9 +36,18 @@ class BIOYOND_PolymerReactionStation_Deck(Deck): class BIOYOND_PolymerPreparationStation_Deck(Deck): - def __init__(self, name: str = "PolymerPreparationStation_Deck") -> None: + def __init__( + self, + name: str = "PolymerPreparationStation_Deck", + size_x: float = 2700.0, + size_y: float = 1080.0, + size_z: float = 1500.0, + category: str = "deck", + setup: bool = False + ) -> None: super().__init__(name=name, size_x=2700.0, size_y=1080.0, size_z=1500.0) - self.warehouses = {} + if setup: + self.setup() def setup(self) -> None: # 添加仓库 diff --git a/unilabos/resources/itemized_carrier.py b/unilabos/resources/itemized_carrier.py index 3205c9dd..997ad068 100644 --- a/unilabos/resources/itemized_carrier.py +++ b/unilabos/resources/itemized_carrier.py @@ -99,7 +99,7 @@ class ItemizedCarrier(ResourcePLR): raise ValueError(f"resource {resource} has no location") if resource is not None: self.child_locations[spot] = resource.location - self.child_size[spot] = {"width": resource.size_x, "height": resource.size_y, "depth": resource.size_z} + self.child_size[spot] = {"width": resource._size_x, "height": resource._size_y, "depth": resource._size_z} else: self.child_locations[spot] = Coordinate.zero() self.child_size[spot] = {"width": 0, "height": 0, "depth": 0} From 54cfaf15f346180df9fa648698f33333f7fae985 Mon Sep 17 00:00:00 2001 From: ZiWei <131428629+ZiWei09@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:25:50 +0800 Subject: [PATCH 17/17] Workstation dev yb2 (#100) * Refactor and extend reaction station action messages * Refactor dispensing station tasks to enhance parameter clarity and add batch processing capabilities - Updated `create_90_10_vial_feeding_task` to include detailed parameters for 90%/10% vial feeding, improving clarity and usability. - Introduced `create_batch_90_10_vial_feeding_task` for batch processing of 90%/10% vial feeding tasks with JSON formatted input. - Added `create_batch_diamine_solution_task` for batch preparation of diamine solution, also utilizing JSON formatted input. - Refined `create_diamine_solution_task` to include additional parameters for better task configuration. - Enhanced schema descriptions and default values for improved user guidance. --- docs/developer_guide/add_action.md | 8 +- .../dispensing_station_bioyond.json | 60 + .../experiments/reaction_station_bioyond.json | 69 + .../workstation/bioyond_studio/bioyond_rpc.py | 1081 ++++++++- .../workstation/bioyond_studio/station.py | 2067 ++++++++++++++++- .../devices/dispensing_station_bioyond.yaml | 506 ++++ .../devices/reaction_station_bioyond.yaml | 384 +++ unilabos_msgs/CMakeLists.txt | 8 +- .../action/ReactionStationDripBack.action | 20 +- .../action/ReactionStationLiquidFeed.action | 11 - .../ReactionStationLiquidFeedBeaker.action | 15 + .../ReactionStationLiquidFeedSolvents.action | 15 + .../ReactionStationLiquidFeedTitration.action | 15 + ...nStationLiquidFeedVialsNonTitration.action | 15 + .../action/ReactionStationProExecu.action | 15 +- .../action/ReactionStationReaTackIn.action | 17 +- .../ReactionStationReactorTakenOut.action | 12 + .../ReactionStationSolidFeedVial.action | 19 +- 18 files changed, 4152 insertions(+), 185 deletions(-) create mode 100644 test/experiments/dispensing_station_bioyond.json create mode 100644 test/experiments/reaction_station_bioyond.json create mode 100644 unilabos/registry/devices/dispensing_station_bioyond.yaml create mode 100644 unilabos/registry/devices/reaction_station_bioyond.yaml delete mode 100644 unilabos_msgs/action/ReactionStationLiquidFeed.action create mode 100644 unilabos_msgs/action/ReactionStationLiquidFeedBeaker.action create mode 100644 unilabos_msgs/action/ReactionStationLiquidFeedSolvents.action create mode 100644 unilabos_msgs/action/ReactionStationLiquidFeedTitration.action create mode 100644 unilabos_msgs/action/ReactionStationLiquidFeedVialsNonTitration.action create mode 100644 unilabos_msgs/action/ReactionStationReactorTakenOut.action diff --git a/docs/developer_guide/add_action.md b/docs/developer_guide/add_action.md index 0e39e119..94a9110a 100644 --- a/docs/developer_guide/add_action.md +++ b/docs/developer_guide/add_action.md @@ -127,16 +127,16 @@ add_action_files( ```bash mamba remove --force ros-humble-unilabos-msgs mamba config set safety_checks disabled # 如果没有提升版本号,会触发md5与网络上md5不一致,是正常现象,因此通过本指令关闭md5检查 -mamba install xxx.conda2 --offline +mamba install xxx.conda --offline ``` ## 常见问题 -**Q: 构建失败怎么办?** +**Q: 构建失败怎么办?** A: 检查 Actions 日志中的错误信息,通常是语法错误或依赖问题。修复后重新推送代码即可自动触发新的构建。 -**Q: 如何测试特定平台?** +**Q: 如何测试特定平台?** A: 在手动触发构建时,在平台选择中只填写你需要的平台,如 `linux-64` 或 `win-64`。 -**Q: 构建包在哪里下载?** +**Q: 构建包在哪里下载?** A: 在 Actions 页面的构建结果中,查找 "Artifacts" 部分,每个平台都有对应的构建包可供下载。 diff --git a/test/experiments/dispensing_station_bioyond.json b/test/experiments/dispensing_station_bioyond.json new file mode 100644 index 00000000..b2f79c80 --- /dev/null +++ b/test/experiments/dispensing_station_bioyond.json @@ -0,0 +1,60 @@ +{ + "nodes": [ + { + "id": "dispensing_station_bioyond", + "name": "dispensing_station_bioyond", + "children": [ + "Bioyond_Dispensing_Deck" + ], + "parent": null, + "type": "device", + "class": "dispensing_station.bioyond", + "config": { + "config": { + "api_key": "DE9BDDA0", + "api_host": "http://192.168.1.200:44388" + }, + "deck": { + "data": { + "_resource_child_name": "Bioyond_Dispensing_Deck", + "_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerPreparationStation_Deck" + } + }, + "station_config": { + "station_type": "dispensing_station", + "enable_dispensing_station": true, + "enable_reaction_station": false, + "station_name": "DispensingStation_001", + "description": "Bioyond配液工作站" + }, + "protocol_type": [] + }, + "data": {} + }, + { + "id": "Bioyond_Dispensing_Deck", + "name": "Bioyond_Dispensing_Deck", + "sample_id": null, + "children": [], + "parent": "dispensing_station_bioyond", + "type": "deck", + "class": "BIOYOND_PolymerPreparationStation_Deck", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "BIOYOND_PolymerPreparationStation_Deck", + "setup": true, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + } + }, + "data": {} + } + ] +} diff --git a/test/experiments/reaction_station_bioyond.json b/test/experiments/reaction_station_bioyond.json new file mode 100644 index 00000000..2a18d90a --- /dev/null +++ b/test/experiments/reaction_station_bioyond.json @@ -0,0 +1,69 @@ +{ + "nodes": [ + { + "id": "reaction_station_bioyond", + "name": "reaction_station_bioyond", + "parent": null, + "children": [ + "Bioyond_Deck" + ], + "type": "device", + "class": "reaction_station.bioyond", + "config": { + "bioyond_config": { + "api_key": "DE9BDDA0", + "api_host": "http://192.168.1.200:44402", + "workflow_mappings": { + "reactor_taken_out": "3a16081e-4788-ca37-eff4-ceed8d7019d1", + "reactor_taken_in": "3a160df6-76b3-0957-9eb0-cb496d5721c6", + "Solid_feeding_vials": "3a160877-87e7-7699-7bc6-ec72b05eb5e6", + "Liquid_feeding_vials(non-titration)": "3a167d99-6158-c6f0-15b5-eb030f7d8e47", + "Liquid_feeding_solvents": "3a160824-0665-01ed-285a-51ef817a9046", + "Liquid_feeding(titration)": "3a160824-0665-01ed-285a-51ef817a9046", + "Liquid_feeding_beaker": "3a16087e-124f-8ddb-8ec1-c2dff09ca784", + "Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a" + }, + "material_type_mappings": { + "烧杯": "BIOYOND_PolymerStation_1FlaskCarrier", + "试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier", + "样品板": "BIOYOND_PolymerStation_6VialCarrier" + } + }, + "deck": { + "data": { + "_resource_child_name": "Bioyond_Deck", + "_resource_type": "unilabos.resources.bioyond.decks:BIOYOND_PolymerReactionStation_Deck" + } + }, + "protocol_type": [] + }, + "data": {} + }, + { + "id": "Bioyond_Deck", + "name": "Bioyond_Deck", + "sample_id": null, + "children": [ + ], + "parent": "reaction_station_bioyond", + "type": "deck", + "class": "BIOYOND_PolymerReactionStation_Deck", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "BIOYOND_PolymerReactionStation_Deck", + "setup": true, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + } + }, + "data": {} + } + ] +} diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py b/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py index f545a2ec..12c03860 100644 --- a/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py +++ b/unilabos/devices/workstation/bioyond_studio/bioyond_rpc.py @@ -50,14 +50,14 @@ class BioyondV1RPC(BaseRequest): self.pending_task_params = [] self.material_cache = {} self._load_material_cache() - + if "workflow_mappings" in config: self._set_workflow_mappings(config["workflow_mappings"]) - + def _set_workflow_mappings(self, mappings: Dict[str, str]): self.workflow_mappings = mappings print(f"设置工作流映射配置: {mappings}") - + def _get_workflow(self, web_workflow_name: str) -> str: if web_workflow_name not in self.workflow_mappings: print(f"未找到工作流映射配置: {web_workflow_name}") @@ -65,7 +65,7 @@ class BioyondV1RPC(BaseRequest): workflow_id = self.workflow_mappings[web_workflow_name] print(f"获取工作流: {web_workflow_name} -> {workflow_id}") return workflow_id - + def process_web_workflows(self, json_str: str) -> Dict[str, str]: try: data = json.loads(json_str) @@ -73,10 +73,10 @@ class BioyondV1RPC(BaseRequest): except json.JSONDecodeError: print(f"无效的JSON字符串: {json_str}") return {} - + result = {} self.workflow_sequence = [] - + for web_name in web_workflow_list: workflow_id = self._get_workflow(web_name) if workflow_id: @@ -84,49 +84,49 @@ class BioyondV1RPC(BaseRequest): self.workflow_sequence.append(workflow_id) else: print(f"无法获取工作流ID: {web_name}") - + print(f"工作流执行顺序: {self.workflow_sequence}") return result - + def get_workflow_sequence(self) -> List[str]: id_to_name = {workflow_id: name for name, workflow_id in self.workflow_mappings.items()} workflow_names = [] for workflow_id in self.workflow_sequence: workflow_names.append(id_to_name.get(workflow_id, workflow_id)) return workflow_names - + def append_to_workflow_sequence(self, json_str: str) -> bool: try: data = json.loads(json_str) web_workflow_name = data.get("web_workflow_name", "") except: return False - + workflow_id = self._get_workflow(web_workflow_name) if workflow_id: self.workflow_sequence.append(workflow_id) print(f"添加工作流到执行顺序: {web_workflow_name} -> {workflow_id}") - + def set_workflow_sequence(self, json_str: str) -> List[str]: try: data = json.loads(json_str) web_workflow_names = data.get("web_workflow_names", []) except: return [] - + sequence = [] for web_name in web_workflow_names: workflow_id = self._get_workflow(web_name) if workflow_id: sequence.append(workflow_id) - + self.workflow_sequence = sequence print(f"设置工作流执行顺序: {self.workflow_sequence}") return self.workflow_sequence.copy() - + def get_all_workflows(self) -> Dict[str, str]: return self.workflow_mappings.copy() - + def clear_workflows(self): self.workflow_sequence = [] print("清空工作流执行顺序") @@ -141,7 +141,7 @@ class BioyondV1RPC(BaseRequest): params = json.loads(json_str) except: return [] - + response = self.post( url=f'{self.host}/api/lims/storage/stock-material', params={ @@ -160,7 +160,7 @@ class BioyondV1RPC(BaseRequest): params = json.loads(json_str) except: return {} - + response = self.post( url=f'{self.host}/api/lims/workflow/work-flow-list', params={ @@ -173,6 +173,134 @@ class BioyondV1RPC(BaseRequest): return {} return response.get("data", {}) + def validate_workflow_parameters(self, workflows: List[Dict[str, Any]]) -> Dict[str, Any]: + """验证工作流参数格式 + + Args: + workflows (List[Dict[str, Any]]): 工作流列表 + + Returns: + Dict[str, Any]: 验证结果 + """ + try: + validation_errors = [] + + for i, workflow in enumerate(workflows): + workflow_errors = [] + + # 检查基本结构 + if not isinstance(workflow, dict): + workflow_errors.append("工作流必须是字典类型") + continue + + if "id" not in workflow: + workflow_errors.append("缺少必要的 'id' 字段") + + # 检查 stepParameters(如果存在) + if "stepParameters" in workflow: + step_params = workflow["stepParameters"] + + if not isinstance(step_params, dict): + workflow_errors.append("stepParameters 必须是字典类型") + else: + # 验证参数结构 + for step_id, modules in step_params.items(): + if not isinstance(modules, dict): + workflow_errors.append(f"步骤 {step_id} 的模块配置必须是字典类型") + continue + + for module_name, params in modules.items(): + if not isinstance(params, list): + workflow_errors.append(f"步骤 {step_id} 模块 {module_name} 的参数必须是列表类型") + continue + + for j, param in enumerate(params): + if not isinstance(param, dict): + workflow_errors.append(f"步骤 {step_id} 模块 {module_name} 参数 {j} 必须是字典类型") + elif "key" not in param or "value" not in param: + workflow_errors.append(f"步骤 {step_id} 模块 {module_name} 参数 {j} 必须包含 key 和 value") + + if workflow_errors: + validation_errors.append({ + "workflow_index": i, + "workflow_id": workflow.get("id", "unknown"), + "errors": workflow_errors + }) + + if validation_errors: + return { + "valid": False, + "errors": validation_errors, + "message": f"发现 {len(validation_errors)} 个工作流存在验证错误" + } + else: + return { + "valid": True, + "message": f"所有 {len(workflows)} 个工作流验证通过" + } + + except Exception as e: + return { + "valid": False, + "errors": [{"general_error": str(e)}], + "message": f"验证过程中发生异常: {str(e)}" + } + + def get_workflow_parameter_template(self) -> Dict[str, Any]: + """获取工作流参数模板 + + Returns: + Dict[str, Any]: 参数模板和说明 + """ + return { + "template": { + "name": "拼接后的长工作流的名称", + "workflows": [ + { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "stepParameters": { + "步骤ID (UUID)": { + "模块名称": [ + { + "key": "参数键名", + "value": "参数值或变量引用 {{index-m-n}}" + } + ] + } + } + } + ] + }, + "parameter_descriptions": { + "name": "拼接后的长工作流名称", + "workflows": "待合并的子工作流列表", + "id": "子工作流 ID,对应工作流列表中 workflows 数组中每个对象的 id 字段", + "stepParameters": "步骤参数配置,如果子工作流没有参数则不需要填写" + }, + "common_modules": { + "反应模块-开始搅拌": { + "description": "反应模块搅拌控制", + "common_parameters": ["temperature"] + }, + "通量-配置": { + "description": "通量配置模块", + "common_parameters": ["cutoff", "assignMaterialName"] + }, + "烧杯溶液放置位-烧杯吸液分液": { + "description": "烧杯液体处理模块", + "common_parameters": ["titrationType", "assignMaterialName", "volume"] + } + }, + "variable_reference_format": { + "format": "{{index-m-n}}", + "description": { + "index": "该步骤所在子工作流的拼接顺序(从 1 开始)", + "m": "拼接前该步骤在子工作流内部的 m 值", + "n": "拼接前该步骤在子工作流内部的 n 值" + } + } + } + # 工作流步骤查询接口 def workflow_step_query(self, json_str: str) -> dict: try: @@ -180,7 +308,7 @@ class BioyondV1RPC(BaseRequest): workflow_id = data.get("workflow_id", "") except: return {} - + response = self.post( url=f'{self.host}/api/lims/workflow/sub-workflow-step-parameters', params={ @@ -200,15 +328,15 @@ class BioyondV1RPC(BaseRequest): except Exception as e: result = str({"success": False, "error": f"create_order:处理JSON时出错: {str(e)}", "method": "create_order"}) return result - + print('===============', json.dumps(params)) - + request_params = { "apiKey": self.api_key, "requestTime": self.get_current_time_iso8601(), "data": params } - + response = self.post( url=f'{self.host}/api/lims/order/order', params=request_params) @@ -226,7 +354,7 @@ class BioyondV1RPC(BaseRequest): params = json.loads(json_str) except: return {} - + response = self.post( url=f'{self.host}/api/lims/order/order-list', params={ @@ -246,7 +374,7 @@ class BioyondV1RPC(BaseRequest): order_id = data.get("order_id", "") except: return {} - + response = self.post( url=f'{self.host}/api/lims/order/order-report', params={ @@ -259,6 +387,32 @@ class BioyondV1RPC(BaseRequest): return {} return response.get("data", {}) + def material_id_query(self, json_str: str) -> dict: + """ + 查询物料id + json_str 格式为JSON字符串: + '{"material123"}' + """ + params = json_str + + response = self.post( + url=f'{self.host}/api/lims/storage/workflow-sample-locations', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response: + return {} + + if response['code'] != 1: + print(f"material_id_query error: {response.get('message')}") + return {} + + print(f"material_id_query data: {response['data']}") + return response.get("data", {}) + # 任务取出接口 def order_takeout(self, json_str: str) -> int: try: @@ -269,7 +423,7 @@ class BioyondV1RPC(BaseRequest): } except: return 0 - + response = self.post( url=f'{self.host}/api/lims/order/order-takeout', params={ @@ -281,7 +435,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return 0 return response.get("code", 0) - + # 设备列表查询 def device_list(self, json_str: str = "") -> list: device_no = None @@ -291,11 +445,11 @@ class BioyondV1RPC(BaseRequest): device_no = data.get("device_no", None) except: pass - + url = f'{self.host}/api/lims/device/device-list' if device_no: url += f'/{device_no}' - + response = self.post( url=url, params={ @@ -306,7 +460,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return [] return response.get("data", []) - + # 设备操作 def device_operation(self, json_str: str) -> int: try: @@ -318,7 +472,7 @@ class BioyondV1RPC(BaseRequest): } except: return 0 - + response = self.post( url=f'{self.host}/api/lims/device/device-operation', params={ @@ -330,7 +484,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return 0 return response.get("code", 0) - + # 调度器状态查询 def scheduler_status(self) -> dict: response = self.post( @@ -343,7 +497,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return {} return response.get("data", {}) - + # 调度器启动 def scheduler_start(self) -> int: response = self.post( @@ -356,7 +510,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return 0 return response.get("code", 0) - + # 调度器暂停 def scheduler_pause(self) -> int: response = self.post( @@ -369,7 +523,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return 0 return response.get("code", 0) - + # 调度器继续 def scheduler_continue(self) -> int: response = self.post( @@ -382,7 +536,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return 0 return response.get("code", 0) - + # 调度器停止 def scheduler_stop(self) -> int: response = self.post( @@ -395,7 +549,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return 0 return response.get("code", 0) - + # 调度器重置 def scheduler_reset(self) -> int: response = self.post( @@ -408,7 +562,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return 0 return response.get("code", 0) - + # 取消任务 def cancel_order(self, json_str: str) -> bool: try: @@ -416,7 +570,7 @@ class BioyondV1RPC(BaseRequest): order_id = data.get("order_id", "") except: return False - + response = self.post( url=f'{self.host}/api/lims/order/cancel-order', params={ @@ -428,7 +582,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return False return True - + # 获取可拼接工作流 def query_split_workflow(self) -> list: response = self.post( @@ -441,7 +595,7 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return [] return str(response.get("data", {})) - + # 合并工作流 def merge_workflow(self, json_str: str) -> dict: try: @@ -452,7 +606,7 @@ class BioyondV1RPC(BaseRequest): } except: return {} - + response = self.post( url=f'{self.host}/api/lims/workflow/merge-workflow', params={ @@ -464,7 +618,89 @@ class BioyondV1RPC(BaseRequest): if not response or response['code'] != 1: return {} return response.get("data", {}) - + + # 合并工作流并设置参数 API + def merge_workflow_with_parameters(self, json_str: str) -> dict: + """合并工作流并设置参数 + + 调用 Bioyond API: /api/lims/workflow/merge-workflow-with-parameters + + Args: + json_str (str): JSON 字符串,包含工作流合并配置数据 + + Returns: + dict: API 响应结果,包含 code、message 和 timestamp + """ + try: + # 解析输入的 JSON 数据 + data = json.loads(json_str) + + # 构造 API 请求参数 + params = { + "name": data.get("name", ""), + "workflows": data.get("workflows", []) + } + + # 验证必要参数 + if not params["name"]: + return { + "code": 0, + "message": "工作流名称不能为空", + "timestamp": int(datetime.now().timestamp() * 1000) + } + + if not params["workflows"]: + return { + "code": 0, + "message": "工作流列表不能为空", + "timestamp": int(datetime.now().timestamp() * 1000) + } + + except json.JSONDecodeError as e: + return { + "code": 0, + "message": f"JSON 解析错误: {str(e)}", + "timestamp": int(datetime.now().timestamp() * 1000) + } + except Exception as e: + return { + "code": 0, + "message": f"参数处理错误: {str(e)}", + "timestamp": int(datetime.now().timestamp() * 1000) + } + + # 发送 POST 请求到 Bioyond API + try: + response = self.post( + url=f'{self.host}/api/lims/workflow/merge-workflow-with-parameters', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params, + }) + + # 处理响应 + if not response: + return { + "code": 0, + "message": "API 请求失败,未收到响应", + "timestamp": int(datetime.now().timestamp() * 1000) + } + + # 返回完整的响应结果 + return { + "code": response.get("code", 0), + "message": response.get("message", ""), + "timestamp": response.get("timestamp", int(datetime.now().timestamp() * 1000)) + } + + except Exception as e: + return { + "code": 0, + "message": f"API 请求异常: {str(e)}", + "timestamp": int(datetime.now().timestamp() * 1000) + } + # 合并当前工作流序列 def merge_sequence_workflow(self, json_str: str) -> dict: try: @@ -472,16 +708,16 @@ class BioyondV1RPC(BaseRequest): name = data.get("name", "合并工作流") except: return {} - + if not self.workflow_sequence: print("工作流序列为空,无法合并") return {} - + params = { "name": name, "workflowIds": self.workflow_sequence } - + response = self.post( url=f'{self.host}/api/lims/workflow/merge-workflow', params={ @@ -498,33 +734,33 @@ class BioyondV1RPC(BaseRequest): def process_and_execute_workflow(self, workflow_name: str, task_name: str) -> dict: web_workflow_list = self.get_workflow_sequence() workflow_name = workflow_name - + pending_params_backup = self.pending_task_params.copy() print(f"保存pending_task_params副本,共{len(pending_params_backup)}个参数") - + # 1. 处理网页工作流列表 print(f"处理网页工作流列表: {web_workflow_list}") web_workflow_json = json.dumps({"web_workflow_list": web_workflow_list}) workflows_result = self.process_web_workflows(web_workflow_json) - + if not workflows_result: error_msg = "处理网页工作流列表失败" print(error_msg) result = str({"success": False, "error": f"process_and_execute_workflow:{error_msg}", "method": "process_and_execute_workflow", "step": "process_web_workflows"}) return result - + # 2. 合并工作流序列 print(f"合并工作流序列,名称: {workflow_name}") merge_json = json.dumps({"name": workflow_name}) merged_workflow = self.merge_sequence_workflow(merge_json) print(f"合并工作流序列结果: {merged_workflow}") - + if not merged_workflow: error_msg = "合并工作流序列失败" print(error_msg) result = str({"success": False, "error": f"process_and_execute_workflow:{error_msg}", "method": "process_and_execute_workflow", "step": "merge_sequence_workflow"}) return result - + # 3. 合并所有参数并创建任务 workflow_name = merged_workflow.get("name", "") workflow_id = merged_workflow.get("subWorkflows", [{}])[0].get("id", "") @@ -532,12 +768,12 @@ class BioyondV1RPC(BaseRequest): workflow_query_json = json.dumps({"workflow_id": workflow_id}) workflow_params_structure = self.workflow_step_query(workflow_query_json) - + self.pending_task_params = pending_params_backup print(f"恢复pending_task_params,共{len(self.pending_task_params)}个参数") - + param_values = self.generate_task_param_values(workflow_params_structure) - + task_params = [{ "orderCode": f"BSO{self.get_current_time_iso8601().replace('-', '').replace('T', '').replace(':', '').replace('.', '')[:14]}", "orderName": f"实验-{self.get_current_time_iso8601()[:10].replace('-', '')}", @@ -546,7 +782,7 @@ class BioyondV1RPC(BaseRequest): "paramValues": param_values, "extendProperties": "" }] - + task_json = json.dumps(task_params) print(f"创建任务参数: {type(task_json)}") result = self.create_order(task_json) @@ -556,11 +792,11 @@ class BioyondV1RPC(BaseRequest): print(error_msg) result = str({"success": False, "error": f"process_and_execute_workflow:{error_msg}", "method": "process_and_execute_workflow", "step": "create_order"}) return result - + print(f"任务创建成功: {result}") self.pending_task_params.clear() print("已清空pending_task_params") - + return { "success": True, "workflow": {"name": workflow_name, "id": workflow_id}, @@ -573,23 +809,23 @@ class BioyondV1RPC(BaseRequest): if not workflow_params_structure: print("workflow_params_structure为空") return {} - + data = workflow_params_structure - + # 从pending_task_params中提取实际参数值,按DisplaySectionName和Key组织 pending_params_by_section = {} print(f"开始处理pending_task_params,共{len(self.pending_task_params)}个任务参数组") - + # 获取工作流执行顺序,用于按顺序匹配参数 workflow_sequence = self.get_workflow_sequence() print(f"工作流执行顺序: {workflow_sequence}") - + workflow_index = 0 - + for i, task_param in enumerate(self.pending_task_params): if 'param_values' in task_param: print(f"处理第{i+1}个任务参数组,包含{len(task_param['param_values'])}个步骤") - + if workflow_index < len(workflow_sequence): current_workflow = workflow_sequence[workflow_index] section_name = WORKFLOW_TO_SECTION_MAP.get(current_workflow) @@ -598,39 +834,39 @@ class BioyondV1RPC(BaseRequest): else: print(f" 警告: 参数组{i+1}超出了工作流序列范围") continue - + if not section_name: print(f" 警告: 工作流{current_workflow}没有对应的DisplaySectionName") continue - + if section_name not in pending_params_by_section: pending_params_by_section[section_name] = {} - + for step_id, param_list in task_param['param_values'].items(): print(f" 步骤ID: {step_id},参数数量: {len(param_list)}") - + for param_item in param_list: key = param_item.get('Key', '') value = param_item.get('Value', '') m = param_item.get('m', 0) n = param_item.get('n', 0) print(f" 参数: {key} = {value} (m={m}, n={n}) -> 分组到{section_name}") - + param_key = f"{section_name}.{key}" if param_key not in pending_params_by_section[section_name]: pending_params_by_section[section_name][param_key] = [] - + pending_params_by_section[section_name][param_key].append({ 'value': value, 'm': m, 'n': n }) - + print(f"pending_params_by_section构建完成,包含{len(pending_params_by_section)}个分组") - + # 收集所有参数,过滤TaskDisplayable为0的项 filtered_params = [] - + for step_id, step_info in data.items(): if isinstance(step_info, list): for step_item in step_info: @@ -638,17 +874,17 @@ class BioyondV1RPC(BaseRequest): for param in param_list: if param.get("TaskDisplayable") == 0: continue - + param_with_step = param.copy() param_with_step['step_id'] = step_id param_with_step['step_name'] = step_item.get("name", "") param_with_step['step_m'] = step_item.get("m", 0) param_with_step['step_n'] = step_item.get("n", 0) filtered_params.append(param_with_step) - + # 按DisplaySectionIndex排序 filtered_params.sort(key=lambda x: x.get('DisplaySectionIndex', 0)) - + # 生成参数映射 param_mapping = {} step_params = {} @@ -657,7 +893,7 @@ class BioyondV1RPC(BaseRequest): if step_id not in step_params: step_params[step_id] = [] step_params[step_id].append(param) - + # 为每个步骤生成参数 for step_id, params in step_params.items(): param_list = [] @@ -666,10 +902,10 @@ class BioyondV1RPC(BaseRequest): display_section_index = param.get('DisplaySectionIndex', 0) step_m = param.get('step_m', 0) step_n = param.get('step_n', 0) - + section_name = param.get('DisplaySectionName', '') param_key = f"{section_name}.{key}" - + if section_name in pending_params_by_section and param_key in pending_params_by_section[section_name]: pending_param_list = pending_params_by_section[section_name][param_key] if pending_param_list: @@ -689,7 +925,7 @@ class BioyondV1RPC(BaseRequest): m = display_section_index n = step_n print(f" 匹配失败: {section_name}.{key},使用默认值 = {value} (m={m}, n={n})") - + param_item = { "m": m, "n": n, @@ -697,10 +933,10 @@ class BioyondV1RPC(BaseRequest): "value": str(value).strip() } param_list.append(param_item) - + if param_list: param_mapping[step_id] = param_list - + print(f"生成任务参数值,包含 {len(param_mapping)} 个步骤") return param_mapping @@ -718,7 +954,7 @@ class BioyondV1RPC(BaseRequest): """反应器放入""" self.append_to_workflow_sequence('{"web_workflow_name": "reactor_taken_in"}') material_id = self._get_material_id_by_name(assign_material_name) - + if isinstance(temperature, str): temperature = float(temperature) @@ -737,8 +973,8 @@ class BioyondV1RPC(BaseRequest): print(f"成功添加反应器放入参数: material={assign_material_name}->ID:{material_id}, cutoff={cutoff}, temp={temperature:.2f}") print(f"当前队列长度: {len(self.pending_task_params)}") return json.dumps({"suc": True}) - - def solid_feeding_vials(self, material_id: str, time: str = "0", torque_variation: str = "1", + + def solid_feeding_vials(self, material_id: str, time: str = "0", torque_variation: str = "1", assign_material_name: str = None, temperature: float = 25.00): """固体进料小瓶""" self.append_to_workflow_sequence('{"web_workflow_name": "Solid_feeding_vials"}') @@ -749,7 +985,7 @@ class BioyondV1RPC(BaseRequest): feeding_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["feeding"] observe_id = WORKFLOW_STEP_IDS["solid_feeding_vials"]["observe"] - + solid_feeding_vials_params = { "param_values": { feeding_id: [ @@ -769,8 +1005,8 @@ class BioyondV1RPC(BaseRequest): print(f"当前队列长度: {len(self.pending_task_params)}") return json.dumps({"suc": True}) - def liquid_feeding_vials_non_titration(self, volumeFormula: str, assign_material_name: str, - titration_type: str = "1", time: str = "0", + def liquid_feeding_vials_non_titration(self, volumeFormula: str, assign_material_name: str, + titration_type: str = "1", time: str = "0", torque_variation: str = "1", temperature: float = 25.00): """液体进料小瓶(非滴定)""" self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_vials(non-titration)"}') @@ -781,7 +1017,7 @@ class BioyondV1RPC(BaseRequest): liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["liquid"] observe_id = WORKFLOW_STEP_IDS["liquid_feeding_vials_non_titration"]["observe"] - + params = { "param_values": { liquid_id: [ @@ -801,7 +1037,7 @@ class BioyondV1RPC(BaseRequest): print(f"成功添加液体进料小瓶(非滴定)参数: volume={volumeFormula}μL, material={assign_material_name}->ID:{material_id}") print(f"当前队列长度: {len(self.pending_task_params)}") return json.dumps({"suc": True}) - + def liquid_feeding_solvents(self, assign_material_name: str, volume: str, titration_type: str = "1", time: str = "360", torque_variation: str = "2", temperature: float = 25.00): """液体进料溶剂""" @@ -813,7 +1049,7 @@ class BioyondV1RPC(BaseRequest): liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["liquid"] observe_id = WORKFLOW_STEP_IDS["liquid_feeding_solvents"]["observe"] - + params = { "param_values": { liquid_id: [ @@ -833,7 +1069,7 @@ class BioyondV1RPC(BaseRequest): print(f"成功添加液体进料溶剂参数: material={assign_material_name}->ID:{material_id}, volume={volume}μL") print(f"当前队列长度: {len(self.pending_task_params)}") return json.dumps({"suc": True}) - + def liquid_feeding_titration(self, volume_formula: str, assign_material_name: str, titration_type: str = "1", time: str = "90", torque_variation: int = 2, temperature: float = 25.00): """液体进料(滴定)""" @@ -845,7 +1081,7 @@ class BioyondV1RPC(BaseRequest): liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["liquid"] observe_id = WORKFLOW_STEP_IDS["liquid_feeding_titration"]["observe"] - + params = { "param_values": { liquid_id: [ @@ -865,9 +1101,9 @@ class BioyondV1RPC(BaseRequest): print(f"成功添加液体进料滴定参数: volume={volume_formula}μL, material={assign_material_name}->ID:{material_id}") print(f"当前队列长度: {len(self.pending_task_params)}") return json.dumps({"suc": True}) - + def liquid_feeding_beaker(self, volume: str = "35000", assign_material_name: str = "BAPP", - time: str = "0", torque_variation: str = "1", titrationType: str = "1", + time: str = "0", torque_variation: str = "1", titrationType: str = "1", temperature: float = 25.00): """液体进料烧杯""" self.append_to_workflow_sequence('{"web_workflow_name": "liquid_feeding_beaker"}') @@ -878,7 +1114,7 @@ class BioyondV1RPC(BaseRequest): liquid_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["liquid"] observe_id = WORKFLOW_STEP_IDS["liquid_feeding_beaker"]["observe"] - + params = { "param_values": { liquid_id: [ @@ -1056,3 +1292,660 @@ class BioyondV1RPC(BaseRequest): def get_logger(self): return self._logger + + # ==================== 配液站特有方法 ==================== + + def sample_waste_removal(self, order_id: str) -> dict: + """ + 样品/废料取出接口 + + 参数: + - order_id: 订单ID + + 返回: 取出结果 + """ + params = {"orderId": order_id} + + response = self.post( + url=f'{self.host}/api/lims/order/take-out', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response: + return {} + + if response['code'] != 1: + self._logger.error(f"样品废料取出错误: {response.get('message', '')}") + return {} + + return response.get("data", {}) + + def dispensing_material_inbound(self, material_id: str, location_id: str) -> dict: + """ + 配液站物料入库接口 + + 参数: + - material_id: 物料ID + - location_id: 库位ID + + 返回: 入库结果 + """ + params = { + "materialId": material_id, + "locationId": location_id + } + + response = self.post( + url=f'{self.host}/api/lims/storage/inbound', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response: + return {} + + if response['code'] != 1: + self._logger.error(f"配液站物料入库错误: {response.get('message', '')}") + return {} + + return response.get("data", {}) + + def dispensing_material_outbound(self, material_id: str, location_id: str, quantity: int) -> dict: + """ + 配液站物料出库接口 + + 参数: + - material_id: 物料ID + - location_id: 库位ID + - quantity: 出库数量 + + 返回: 出库结果 + """ + params = { + "materialId": material_id, + "locationId": location_id, + "quantity": quantity + } + + response = self.post( + url=f'{self.host}/api/lims/storage/outbound', + params={ + "apiKey": self.api_key, + "requestTime": self.get_current_time_iso8601(), + "data": params + }) + + if not response: + return {} + + if response['code'] != 1: + self._logger.error(f"配液站物料出库错误: {response.get('message', '')}") + return {} + + return response.get("data", {}) + + def create_90_10_vial_feeding_task(self, + order_name: str = None, + speed: str = None, + temperature: str = None, + delay_time: str = None, + percent_90_1_assign_material_name: str = None, + percent_90_1_target_weigh: str = None, + percent_90_2_assign_material_name: str = None, + percent_90_2_target_weigh: str = None, + percent_90_3_assign_material_name: str = None, + percent_90_3_target_weigh: str = None, + percent_10_1_assign_material_name: str = None, + percent_10_1_target_weigh: str = None, + percent_10_1_volume: str = None, + percent_10_1_liquid_material_name: str = None, + percent_10_2_assign_material_name: str = None, + percent_10_2_target_weigh: str = None, + percent_10_2_volume: str = None, + percent_10_2_liquid_material_name: str = None, + percent_10_3_assign_material_name: str = None, + percent_10_3_target_weigh: str = None, + percent_10_3_volume: str = None, + percent_10_3_liquid_material_name: str = None, + hold_m_name: str = None) -> dict: + """ + 创建90%10%小瓶投料任务 + + 参数说明: + - order_name: 任务名称,如果为None则使用默认名称 + - speed: 搅拌速度,如果为None则使用默认值400 + - temperature: 温度,如果为None则使用默认值40 + - delay_time: 延迟时间,如果为None则使用默认值600 + - percent_90_1_assign_material_name: 90%_1物料名称 + - percent_90_1_target_weigh: 90%_1目标重量 + - percent_90_2_assign_material_name: 90%_2物料名称 + - percent_90_2_target_weigh: 90%_2目标重量 + - percent_90_3_assign_material_name: 90%_3物料名称 + - percent_90_3_target_weigh: 90%_3目标重量 + - percent_10_1_assign_material_name: 10%_1固体物料名称 + - percent_10_1_target_weigh: 10%_1固体目标重量 + - percent_10_1_volume: 10%_1液体体积 + - percent_10_1_liquid_material_name: 10%_1液体物料名称 + - percent_10_2_assign_material_name: 10%_2固体物料名称 + - percent_10_2_target_weigh: 10%_2固体目标重量 + - percent_10_2_volume: 10%_2液体体积 + - percent_10_2_liquid_material_name: 10%_2液体物料名称 + - percent_10_3_assign_material_name: 10%_3固体物料名称 + - percent_10_3_target_weigh: 10%_3固体目标重量 + - percent_10_3_volume: 10%_3液体体积 + - percent_10_3_liquid_material_name: 10%_3液体物料名称 + - hold_m_name: 库位名称,如"C01",用于查找对应的holdMId + + 返回: 任务创建结果 + """ + # 设置默认值 + if order_name is None: + order_name = f"90%10%小瓶投料任务_{self.get_current_time_iso8601()}" + if speed is None: + speed = "400" + if temperature is None: + temperature = "20" + if delay_time is None: + delay_time = "600" + + # 获取工作流ID + workflow_id = "3a19310d-16b9-9d81-b109-0748e953694b" # 90%10%小瓶投料工作流ID + + # 查询holdMId + holdMId = None + if hold_m_name: + holdMId_response = self.material_id_query(hold_m_name) + if holdMId_response: + holdMId = holdMId_response + + # 构建订单数据 + order_data = [{ + "code": order_name, + "Name": "90%10%小瓶投料任务", + "workflowName": "90%10%小瓶投料", + "borderNumber": 1, + "paramValues": { + workflow_id: [ + # 搅拌速度 + {"m": 3, "n": 2, "key": "speed", "value": speed}, + # 温度 + {"m": 3, "n": 2, "key": "temperature", "value": temperature}, + # 延迟时间 + {"m": 3, "n": 2, "key": "delayTime", "value": delay_time}, + # 90%_1固体物料 + {"m": 3, "n": 2, "key": "90%_1_assignMaterialName", "value": percent_90_1_assign_material_name}, + {"m": 3, "n": 2, "key": "90%_1_targetWeigh", "value": percent_90_1_target_weigh}, + # 90%_2固体物料 + {"m": 3, "n": 2, "key": "90%_2_assignMaterialName", "value": percent_90_2_assign_material_name}, + {"m": 3, "n": 2, "key": "90%_2_targetWeigh", "value": percent_90_2_target_weigh}, + # 90%_3固体物料 + {"m": 3, "n": 2, "key": "90%_3_assignMaterialName", "value": percent_90_3_assign_material_name}, + {"m": 3, "n": 2, "key": "90%_3_targetWeigh", "value": percent_90_3_target_weigh}, + # 10%_1液体物料 + {"m": 3, "n": 2, "key": "10%_1_assignMaterialName", "value": percent_10_1_assign_material_name}, + {"m": 3, "n": 2, "key": "10%_1_targetWeigh", "value": percent_10_1_target_weigh}, + {"m": 3, "n": 2, "key": "10%_1_volume", "value": percent_10_1_volume}, + {"m": 3, "n": 2, "key": "10%_1_liquidMaterialName", "value": percent_10_1_liquid_material_name}, + # 10%_2液体物料 + {"m": 3, "n": 2, "key": "10%_2_assignMaterialName", "value": percent_10_2_assign_material_name}, + {"m": 3, "n": 2, "key": "10%_2_targetWeigh", "value": percent_10_2_target_weigh}, + {"m": 3, "n": 2, "key": "10%_2_volume", "value": percent_10_2_volume}, + {"m": 3, "n": 2, "key": "10%_2_liquidMaterialName", "value": percent_10_2_liquid_material_name}, + # 10%_3液体物料 + {"m": 3, "n": 2, "key": "10%_3_assignMaterialName", "value": percent_10_3_assign_material_name}, + {"m": 3, "n": 2, "key": "10%_3_targetWeigh", "value": percent_10_3_target_weigh}, + {"m": 3, "n": 2, "key": "10%_3_volume", "value": percent_10_3_volume}, + {"m": 3, "n": 2, "key": "10%_3_liquidMaterialName", "value": percent_10_3_liquid_material_name} + ] + }, + "ExtendProperties": f"{{{holdMId}:null}}" if holdMId else "{}" + }] + + try: + # 调用create_order方法创建任务 + result = self.create_order(json.dumps(order_data, ensure_ascii=False)) + self._logger.info(f"90%10%小瓶投料任务创建成功: {result}") + return result + + except Exception as e: + error_msg = f"90%10%小瓶投料任务创建异常: {str(e)}" + self._logger.error(error_msg) + return {"error": error_msg} + + def create_diamine_solution_task(self, + order_name: str = None, + material_name: str = None, + target_weigh: str = None, + volume: str = None, + liquid_material_name: str = "NMP", + speed: str = None, + temperature: str = None, + delay_time: str = None, + hold_m_name: str = None) -> dict: + """ + 创建二胺溶液配置任务 + + 参数说明: + - order_name: 任务名称,如果为None则使用默认名称 + - material_name: 固体物料名称,必填 + - target_weigh: 固体目标重量,必填 + - volume: 液体体积,必填 + - liquid_material_name: 液体物料名称,默认为NMP + - speed: 搅拌速度,如果为None则使用默认值400 + - temperature: 温度,如果为None则使用默认值25 + - delay_time: 延迟时间,如果为None则使用默认值600 + - hold_m_name: 库位名称,如"ODA-1",用于查找对应的holdMId + + 返回: 任务创建结果 + """ + # 验证必填参数 + if not material_name or not target_weigh or not volume: + return { + "status": "error", + "message": "material_name、target_weigh和volume为必填参数" + } + + # 设置默认值 + if order_name is None: + order_name = f"二胺溶液配置任务_{self.get_current_time_iso8601()}" + if speed is None: + speed = "400" + if temperature is None: + temperature = "25" + if delay_time is None: + delay_time = "600" + + # 获取工作流ID + workflow_id = "1" + + # 查询holdMId + holdMId = None + if hold_m_name: + try: + material_query_params = json.dumps({"materialName": hold_m_name}) + material_response = self.material_id_query(material_query_params) + if material_response and material_response.get("code") == 1: + data = material_response.get("data", []) + if data: + holdMId = data[0].get("id") + self._logger.info(f"查询到holdMId: {holdMId} for {hold_m_name}") + else: + self._logger.warning(f"未找到物料: {hold_m_name}") + else: + self._logger.error(f"查询物料ID失败: {material_response}") + except Exception as e: + self._logger.error(f"查询holdMId时发生错误: {e}") + + # 构建order_data + order_data = { + "workflowId": workflow_id, + "orderName": order_name, + "params": { + "1": speed, # 搅拌速度 + "2": temperature, # 温度 + "3": delay_time, # 延迟时间 + "4": material_name, # 固体物料名称 + "5": target_weigh, # 固体目标重量 + "6": volume, # 液体体积 + "7": liquid_material_name # 液体物料名称 + } + } + + if holdMId: + order_data["holdMId"] = holdMId + + try: + # 使用create_order方法创建任务 + order_params = json.dumps(order_data) + response = self.create_order(order_params) + return response + except Exception as e: + self._logger.error(f"创建二胺溶液配置任务时发生错误: {e}") + return {"status": "error", "message": f"创建任务失败: {str(e)}"} + + def create_batch_90_10_vial_feeding_task(self, json_str: str) -> dict: + """ + 创建批量90%10%小瓶投料任务 + + 接受JSON输入,支持多个90%10%小瓶投料任务的批量创建 + + JSON格式示例: + { + "batch_name": "批量90%10%小瓶投料任务_20240101", + "tasks": [ + { + "order_name": "小瓶投料任务1", + "hold_m_name": "C01", + "percent_90_1_assign_material_name": "物料A", + "percent_90_1_target_weigh": "10.5", + "percent_10_1_assign_material_name": "物料B", + "percent_10_1_target_weigh": "5.2", + "percent_10_1_volume": "50.0", + "percent_10_1_liquid_material_name": "NMP", + "speed": "400", + "temperature": "40", + "delay_time": "600" + } + ], + "global_settings": { + "speed": "400", + "temperature": "40", + "delay_time": "600" + } + } + + 参数说明: + - batch_name: 批量任务名称,可选 + - tasks: 任务列表,每个任务包含90%10%小瓶投料参数 + - global_settings: 全局默认设置,当单个任务未指定参数时使用 + + 返回: 批量任务创建结果 + """ + try: + # 解析JSON输入 + data = json.loads(json_str) + + # 获取批量任务参数 + batch_name = data.get("batch_name", f"批量90%10%小瓶投料任务_{self.get_current_time_iso8601()}") + tasks = data.get("tasks", []) + global_settings = data.get("global_settings", {}) + + if not tasks: + return { + "status": "error", + "message": "任务列表不能为空" + } + + # 批量创建结果 + batch_results = { + "batch_name": batch_name, + "total_tasks": len(tasks), + "successful_tasks": 0, + "failed_tasks": 0, + "task_results": [] + } + + self._logger.info(f"开始创建批量90%10%小瓶投料任务: {batch_name}, 包含 {len(tasks)} 个子任务") + + # 逐个创建任务 + for i, task in enumerate(tasks): + try: + # 合并全局设置和任务特定设置 + task_params = {**global_settings, **task} + + # 验证必填参数 - hold_m_name是必须的 + if not task_params.get("hold_m_name"): + error_msg = f"任务 {i+1} 缺少必填参数: hold_m_name" + self._logger.error(error_msg) + batch_results["task_results"].append({ + "task_index": i + 1, + "status": "error", + "message": error_msg + }) + batch_results["failed_tasks"] += 1 + continue + + # 设置任务名称 + if not task_params.get("order_name"): + task_params["order_name"] = f"{batch_name}_任务{i+1}" + + # 调用单个90%10%小瓶投料任务创建方法 + task_result = self.create_90_10_vial_feeding_task( + order_name=task_params.get("order_name"), + speed=task_params.get("speed"), + temperature=task_params.get("temperature"), + delay_time=task_params.get("delay_time"), + percent_90_1_assign_material_name=task_params.get("percent_90_1_assign_material_name"), + percent_90_1_target_weigh=task_params.get("percent_90_1_target_weigh"), + percent_90_2_assign_material_name=task_params.get("percent_90_2_assign_material_name"), + percent_90_2_target_weigh=task_params.get("percent_90_2_target_weigh"), + percent_90_3_assign_material_name=task_params.get("percent_90_3_assign_material_name"), + percent_90_3_target_weigh=task_params.get("percent_90_3_target_weigh"), + percent_10_1_assign_material_name=task_params.get("percent_10_1_assign_material_name"), + percent_10_1_target_weigh=task_params.get("percent_10_1_target_weigh"), + percent_10_1_volume=task_params.get("percent_10_1_volume"), + percent_10_1_liquid_material_name=task_params.get("percent_10_1_liquid_material_name"), + percent_10_2_assign_material_name=task_params.get("percent_10_2_assign_material_name"), + percent_10_2_target_weigh=task_params.get("percent_10_2_target_weigh"), + percent_10_2_volume=task_params.get("percent_10_2_volume"), + percent_10_2_liquid_material_name=task_params.get("percent_10_2_liquid_material_name"), + percent_10_3_assign_material_name=task_params.get("percent_10_3_assign_material_name"), + percent_10_3_target_weigh=task_params.get("percent_10_3_target_weigh"), + percent_10_3_volume=task_params.get("percent_10_3_volume"), + percent_10_3_liquid_material_name=task_params.get("percent_10_3_liquid_material_name"), + hold_m_name=task_params.get("hold_m_name") + ) + + # 记录任务结果 + if isinstance(task_result, dict) and task_result.get("status") != "error": + batch_results["successful_tasks"] += 1 + batch_results["task_results"].append({ + "task_index": i + 1, + "task_name": task_params.get("order_name"), + "status": "success", + "result": task_result + }) + self._logger.info(f"任务 {i+1} 创建成功: {task_params.get('order_name')}") + else: + batch_results["failed_tasks"] += 1 + batch_results["task_results"].append({ + "task_index": i + 1, + "task_name": task_params.get("order_name"), + "status": "error", + "message": str(task_result) + }) + self._logger.error(f"任务 {i+1} 创建失败: {task_result}") + + except Exception as e: + error_msg = f"任务 {i+1} 处理时发生异常: {str(e)}" + self._logger.error(error_msg) + batch_results["failed_tasks"] += 1 + batch_results["task_results"].append({ + "task_index": i + 1, + "status": "error", + "message": error_msg + }) + + # 设置批量任务整体状态 + if batch_results["failed_tasks"] == 0: + batch_results["status"] = "success" + batch_results["message"] = f"批量90%10%小瓶投料任务全部创建成功,共 {batch_results['successful_tasks']} 个任务" + elif batch_results["successful_tasks"] == 0: + batch_results["status"] = "error" + batch_results["message"] = f"批量90%10%小瓶投料任务全部创建失败,共 {batch_results['failed_tasks']} 个任务" + else: + batch_results["status"] = "partial_success" + batch_results["message"] = f"批量90%10%小瓶投料任务部分成功,成功 {batch_results['successful_tasks']} 个,失败 {batch_results['failed_tasks']} 个" + + self._logger.info(f"批量90%10%小瓶投料任务完成: {batch_results['message']}") + return batch_results + + except json.JSONDecodeError as e: + error_msg = f"JSON解析失败: {str(e)}" + self._logger.error(error_msg) + return {"status": "error", "message": error_msg} + except Exception as e: + error_msg = f"创建批量90%10%小瓶投料任务时发生错误: {str(e)}" + self._logger.error(error_msg) + return {"status": "error", "message": error_msg} + + def create_batch_diamine_solution_task(self, json_str: str) -> dict: + """ + 创建批量二胺溶液配制任务 + + 接受JSON输入,支持多个二胺溶液配制任务的批量创建 + + JSON格式示例: + { + "batch_name": "批量二胺溶液配制任务_20240101", + "tasks": [ + { + "order_name": "二胺溶液配制任务1", + "material_name": "物料A", + "target_weigh": "10.5", + "volume": "50.0", + "liquid_material_name": "NMP", + "speed": "400", + "temperature": "25", + "delay_time": "600", + "hold_m_name": "A01" + }, + { + "order_name": "二胺溶液配制任务2", + "material_name": "物料B", + "target_weigh": "15.2", + "volume": "75.0", + "liquid_material_name": "DMF", + "speed": "350", + "temperature": "30", + "delay_time": "800", + "hold_m_name": "B02" + } + ], + "global_settings": { + "speed": "400", + "temperature": "25", + "delay_time": "600", + "liquid_material_name": "NMP" + } + } + + 参数说明: + - batch_name: 批量任务名称,可选 + - tasks: 任务列表,每个任务包含二胺溶液配制参数 + - global_settings: 全局默认设置,当单个任务未指定参数时使用 + + 每个任务参数: + - order_name: 任务名称 + - material_name: 物料名称,必填 + - target_weigh: 目标重量,必填 + - volume: 体积,必填 + - liquid_material_name: 液体物料名称,可选 + - speed: 搅拌速度,可选 + - temperature: 温度,可选 + - delay_time: 延迟时间,可选 + - hold_m_name: 库位名称,可选 + + 返回: 批量任务创建结果 + """ + try: + # 解析JSON输入 + data = json.loads(json_str) + + # 获取批量任务参数 + batch_name = data.get("batch_name", f"批量二胺溶液配制任务_{self.get_current_time_iso8601()}") + tasks = data.get("tasks", []) + global_settings = data.get("global_settings", {}) + + if not tasks: + return { + "status": "error", + "message": "任务列表不能为空" + } + + # 批量创建结果 + batch_results = { + "batch_name": batch_name, + "total_tasks": len(tasks), + "successful_tasks": 0, + "failed_tasks": 0, + "task_results": [] + } + + self._logger.info(f"开始创建批量二胺溶液配制任务: {batch_name}, 包含 {len(tasks)} 个子任务") + + # 逐个创建任务 + for i, task in enumerate(tasks): + try: + # 合并全局设置和任务特定设置 + task_params = {**global_settings, **task} + + # 验证必填参数 + required_params = ["material_name", "target_weigh", "volume"] + missing_params = [param for param in required_params if not task_params.get(param)] + + if missing_params: + error_msg = f"任务 {i+1} 缺少必填参数: {', '.join(missing_params)}" + self._logger.error(error_msg) + batch_results["task_results"].append({ + "task_index": i + 1, + "status": "error", + "message": error_msg + }) + batch_results["failed_tasks"] += 1 + continue + + # 设置任务名称 + if not task_params.get("order_name"): + task_params["order_name"] = f"{batch_name}_任务{i+1}" + + # 调用单个二胺溶液配制任务创建方法 + task_result = self.create_diamine_solution_task( + order_name=task_params.get("order_name"), + material_name=task_params.get("material_name"), + target_weigh=task_params.get("target_weigh"), + volume=task_params.get("volume"), + liquid_material_name=task_params.get("liquid_material_name", "NMP"), + speed=task_params.get("speed"), + temperature=task_params.get("temperature"), + delay_time=task_params.get("delay_time"), + hold_m_name=task_params.get("hold_m_name") + ) + + # 记录任务结果 + if isinstance(task_result, dict) and task_result.get("status") != "error": + batch_results["successful_tasks"] += 1 + batch_results["task_results"].append({ + "task_index": i + 1, + "task_name": task_params.get("order_name"), + "status": "success", + "result": task_result + }) + self._logger.info(f"任务 {i+1} 创建成功: {task_params.get('order_name')}") + else: + batch_results["failed_tasks"] += 1 + batch_results["task_results"].append({ + "task_index": i + 1, + "task_name": task_params.get("order_name"), + "status": "error", + "message": str(task_result) + }) + self._logger.error(f"任务 {i+1} 创建失败: {task_result}") + + except Exception as e: + error_msg = f"滴定液任务 {i+1} 处理时发生异常: {str(e)}" + self._logger.error(error_msg) + batch_results["failed_tasks"] += 1 + batch_results["task_results"].append({ + "task_index": i + 1, + "status": "error", + "message": error_msg + }) + + # 设置批量任务整体状态 + if batch_results["failed_tasks"] == 0: + batch_results["status"] = "success" + batch_results["message"] = f"批量滴定液任务全部创建成功,共 {batch_results['successful_tasks']} 个任务" + elif batch_results["successful_tasks"] == 0: + batch_results["status"] = "error" + batch_results["message"] = f"批量滴定液任务全部创建失败,共 {batch_results['failed_tasks']} 个任务" + else: + batch_results["status"] = "partial_success" + batch_results["message"] = f"批量滴定液任务部分成功,成功 {batch_results['successful_tasks']} 个,失败 {batch_results['failed_tasks']} 个" + + self._logger.info(f"批量滴定液任务完成: {batch_results['message']}") + return batch_results + + except json.JSONDecodeError as e: + error_msg = f"JSON解析失败: {str(e)}" + self._logger.error(error_msg) + return {"status": "error", "message": error_msg} + except Exception as e: + error_msg = f"创建批量滴定液任务时发生错误: {str(e)}" + self._logger.error(error_msg) + return {"status": "error", "message": error_msg} diff --git a/unilabos/devices/workstation/bioyond_studio/station.py b/unilabos/devices/workstation/bioyond_studio/station.py index 3685910a..d09a8a3e 100644 --- a/unilabos/devices/workstation/bioyond_studio/station.py +++ b/unilabos/devices/workstation/bioyond_studio/station.py @@ -17,7 +17,10 @@ from unilabos.resources.graphio import resource_bioyond_to_plr from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode -from unilabos.devices.workstation.bioyond_studio.config import API_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS +from unilabos.devices.workstation.bioyond_studio.config import ( + API_CONFIG, WORKFLOW_MAPPINGS, MATERIAL_TYPE_MAPPINGS, + STATION_TYPES, DEFAULT_STATION_CONFIG +) class BioyondResourceSynchronizer(ResourceSynchronizer): @@ -101,14 +104,15 @@ class BioyondResourceSynchronizer(ResourceSynchronizer): class BioyondWorkstation(WorkstationBase): """Bioyond工作站 - + 集成Bioyond物料管理的工作站实现 """ - + def __init__( self, bioyond_config: Optional[Dict[str, Any]] = None, deck: Optional[Any] = None, + station_config: Optional[Dict[str, Any]] = None, *args, **kwargs, ): @@ -119,18 +123,29 @@ class BioyondWorkstation(WorkstationBase): *args, **kwargs, ) + + # 检查 deck 是否为 None,防止 AttributeError + if self.deck is None: + logger.error("❌ Deck 配置为空,请检查配置文件中的 deck 参数") + raise ValueError("Deck 配置不能为空,请在配置文件中添加正确的 deck 配置") + + # 初始化 warehouses 属性 self.deck.warehouses = {} for resource in self.deck.children: if isinstance(resource, WareHouse): self.deck.warehouses[resource.name] = resource - + + # 配置站点类型 + self._configure_station_type(station_config) + + # 创建通信模块 self._create_communication_module(bioyond_config) self.resource_synchronizer = BioyondResourceSynchronizer(self) self.resource_synchronizer.sync_from_external() - + # TODO: self._ros_node里面拿属性 logger.info(f"Bioyond工作站初始化完成") - + def post_init(self, ros_node: ROS2WorkstationNode): self._ros_node = ros_node #self.deck = create_a_coin_cell_deck() @@ -138,6 +153,47 @@ class BioyondWorkstation(WorkstationBase): "resources": [self.deck] }) + def _configure_station_type(self, station_config: Optional[Dict[str, Any]] = None) -> None: + """配置站点类型和功能模块 + + Args: + station_config (Optional[Dict[str, Any]]): 站点配置,如果为None则使用默认配置 + """ + # 合并默认配置和用户配置 + self.station_config = {**DEFAULT_STATION_CONFIG} + if station_config: + self.station_config.update(station_config) + + # 设置站点属性 + self.station_type = self.station_config["station_type"] + self.enable_reaction_station = self.station_config["enable_reaction_station"] + self.enable_dispensing_station = self.station_config["enable_dispensing_station"] + self.station_name = self.station_config["station_name"] + self.station_description = self.station_config["description"] + + # 根据站点类型调整功能启用状态 + if self.station_type == STATION_TYPES["REACTION"]: + self.enable_reaction_station = True + self.enable_dispensing_station = False + self.station_description = "Bioyond反应站" + logger.info("🧪 配置为反应站模式") + + elif self.station_type == STATION_TYPES["DISPENSING"]: + self.enable_reaction_station = False + self.enable_dispensing_station = True + self.station_description = "Bioyond配液站" + logger.info("🧫 配置为配液站模式") + + elif self.station_type == STATION_TYPES["HYBRID"]: + self.enable_reaction_station = True + self.enable_dispensing_station = True + self.station_description = "Bioyond混合工作站" + logger.info("🔬 配置为混合工作站模式") + + logger.info(f"站点配置: {self.station_name} - {self.station_description}") + logger.info(f"反应站功能: {'✅ 启用' if self.enable_reaction_station else '❌ 禁用'}") + logger.info(f"配液站功能: {'✅ 启用' if self.enable_dispensing_station else '❌ 禁用'}") + def _create_communication_module(self, config: Optional[Dict[str, Any]] = None) -> None: """创建Bioyond通信模块""" self.bioyond_config = config or { @@ -145,14 +201,159 @@ class BioyondWorkstation(WorkstationBase): "workflow_mappings": WORKFLOW_MAPPINGS, "material_type_mappings": MATERIAL_TYPE_MAPPINGS } - self.hardware_interface = BioyondV1RPC(self.bioyond_config) - + + # 根据站点配置有条件地初始化接口 + self.hardware_interface = None + self.dispensing_interface = None + + if self.enable_reaction_station: + # 反应站接口 + self.hardware_interface = BioyondV1RPC(self.bioyond_config) + logger.info("✅ 反应站接口已初始化") + else: + logger.info("⏭️ 反应站接口已跳过") + + if self.enable_dispensing_station: + # 配液站接口 - 使用统一的BioyondV1RPC类 + self.dispensing_interface = BioyondV1RPC(self.bioyond_config) + logger.info("✅ 配液站接口已初始化") + else: + logger.info("⏭️ 配液站接口已跳过") + return None - + + def _check_interface_availability(self, interface_type: str) -> bool: + """检查指定接口是否可用 + + Args: + interface_type (str): 接口类型,'reaction' 或 'dispensing' + + Returns: + bool: 接口是否可用 + + Raises: + RuntimeError: 当接口不可用时抛出异常 + """ + if interface_type == "reaction": + if not self.enable_reaction_station or self.hardware_interface is None: + raise RuntimeError( + f"❌ 反应站接口不可用!当前站点类型: {self.station_type}, " + f"反应站功能: {'启用' if self.enable_reaction_station else '禁用'}" + ) + return True + + elif interface_type == "dispensing": + if not self.enable_dispensing_station or self.dispensing_interface is None: + raise RuntimeError( + f"❌ 配液站接口不可用!当前站点类型: {self.station_type}, " + f"配液站功能: {'启用' if self.enable_dispensing_station else '禁用'}" + ) + return True + + else: + raise ValueError(f"未知的接口类型: {interface_type}") + + def get_station_info(self) -> Dict[str, Any]: + """获取站点信息 + + Returns: + Dict[str, Any]: 站点配置和状态信息 + """ + return { + "station_name": self.station_name, + "station_type": self.station_type, + "station_description": self.station_description, + "enable_reaction_station": self.enable_reaction_station, + "enable_dispensing_station": self.enable_dispensing_station, + "reaction_interface_available": self.hardware_interface is not None, + "dispensing_interface_available": self.dispensing_interface is not None, + "supported_station_types": list(STATION_TYPES.values()) + } + + @property + def bioyond_status(self) -> Dict[str, Any]: + """获取 Bioyond 系统状态信息 + + 这个属性被 ROS 节点用来发布设备状态 + + Returns: + Dict[str, Any]: Bioyond 系统的状态信息 + """ + try: + # 获取基础站点信息 + station_info = self.get_station_info() + + # 获取接口状态 + interface_status = { + "reaction_interface_connected": False, + "dispensing_interface_connected": False, + "last_sync_time": getattr(self.resource_synchronizer, 'last_sync_time', 0), + "sync_interval": getattr(self.resource_synchronizer, 'sync_interval', 60) + } + + # 检查反应站接口状态 + if self.hardware_interface is not None: + try: + # 尝试获取调度器状态来验证连接 + scheduler_status = self.get_scheduler_status() + interface_status["reaction_interface_connected"] = scheduler_status.get("status") == "success" + except Exception: + interface_status["reaction_interface_connected"] = False + + # 检查配液站接口状态 + if self.dispensing_interface is not None: + try: + # 配液站接口也使用相同的连接检查方式 + interface_status["dispensing_interface_connected"] = True + except Exception: + interface_status["dispensing_interface_connected"] = False + + # 获取资源同步状态 + sync_status = { + "last_sync_success": True, # 默认值,可以根据实际同步结果更新 + "total_resources": len(getattr(self.deck, 'children', [])), + "warehouse_count": len(getattr(self.deck, 'warehouses', {})) + } + + return { + "station_info": station_info, + "interface_status": interface_status, + "sync_status": sync_status, + "timestamp": __import__('time').time(), + "status": "online" if (interface_status["reaction_interface_connected"] or + interface_status["dispensing_interface_connected"]) else "offline" + } + + except Exception as e: + logger.error(f"获取 Bioyond 状态失败: {e}") + # 返回基础状态信息,避免完全失败 + return { + "station_info": { + "station_name": getattr(self, 'station_name', 'BioyondWorkstation'), + "station_type": getattr(self, 'station_type', 'unknown'), + "enable_reaction_station": getattr(self, 'enable_reaction_station', False), + "enable_dispensing_station": getattr(self, 'enable_dispensing_station', False) + }, + "interface_status": { + "reaction_interface_connected": False, + "dispensing_interface_connected": False, + "last_sync_time": 0, + "sync_interval": 60 + }, + "sync_status": { + "last_sync_success": False, + "total_resources": 0, + "warehouse_count": 0 + }, + "timestamp": __import__('time').time(), + "status": "error", + "error_message": str(e) + } + def _register_supported_workflows(self): """注册Bioyond支持的工作流""" from unilabos.devices.workstation.workstation_base import WorkflowInfo - + # Bioyond物料同步工作流 self.supported_workflows["bioyond_sync"] = WorkflowInfo( name="bioyond_sync", @@ -162,7 +363,7 @@ class BioyondWorkstation(WorkstationBase): "force_sync": {"type": "boolean", "default": False} } ) - + # Bioyond物料更新工作流 self.supported_workflows["bioyond_update"] = WorkflowInfo( name="bioyond_update", @@ -172,27 +373,27 @@ class BioyondWorkstation(WorkstationBase): "sync_all": {"type": "boolean", "default": True} } ) - + logger.info(f"注册了 {len(self.supported_workflows)} 个Bioyond工作流") - + async def execute_bioyond_sync_workflow(self, parameters: Dict[str, Any]) -> Dict[str, Any]: """执行Bioyond同步工作流""" try: sync_type = parameters.get("sync_type", "full") force_sync = parameters.get("force_sync", False) - + logger.info(f"开始执行Bioyond同步工作流: {sync_type}") - + # 获取物料管理模块 material_manager = self.material_management - + if sync_type == "full": # 全量同步 success = await material_manager.sync_from_bioyond() else: # 增量同步(这里可以实现增量同步逻辑) success = await material_manager.sync_from_bioyond() - + if success: result = { "status": "success", @@ -204,28 +405,1806 @@ class BioyondWorkstation(WorkstationBase): "status": "failed", "message": "Bioyond同步失败" } - + logger.info(f"Bioyond同步工作流执行完成: {result['status']}") return result - + except Exception as e: logger.error(f"Bioyond同步工作流执行失败: {e}") return { "status": "error", "message": str(e) } - + + # ==================== 工作流合并与参数设置 API ==================== + + def merge_workflow_with_parameters( + self, + name: str, + workflows: List[Dict[str, Any]], + **kwargs + ) -> Dict[str, Any]: + """合并工作流并设置参数 API + + 合并子工作流时传入实验参数,新建实验时如果没有传参数,则使用此处传入的参数作为默认值 + + Args: + name (str): 拼接后的长工作流名称 + workflows (List[Dict[str, Any]]): 待合并的子工作流列表,每个元素包含: + - id (str): 子工作流 ID (UUID) + - stepParameters (Dict, 可选): 步骤参数配置 + **kwargs: 其他参数 + + Returns: + Dict[str, Any]: 操作结果,包含 code、message 和 timestamp + + Example: + workflows = [ + { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6" + }, + { + "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", + "stepParameters": { + "5a30bee1-7de2-45de-a89f-a25c78e4404b": { + "反应模块-开始搅拌": [ + { + "key": "temperature", + "value": "25" + } + ], + "通量-配置": [ + { + "key": "cutoff", + "value": "9999" + }, + { + "key": "assignMaterialName", + "value": "3a1bf167-e862-f269-3749-a1c70cbbe6a6" + } + ] + } + } + } + ] + + result = workstation.merge_workflow_with_parameters( + name="拼接后的长工作流的名称", + workflows=workflows + ) + """ + try: + # 检查反应站接口是否可用 + self._check_interface_availability("reaction") + + logger.info(f"开始合并工作流: {name}, 包含 {len(workflows)} 个子工作流") + + # 基本参数验证 + if not name: + raise ValueError("工作流名称不能为空") + + if not workflows or len(workflows) == 0: + raise ValueError("工作流列表不能为空") + + # 使用 RPC 层进行详细的参数验证 + validation_result = self.hardware_interface.validate_workflow_parameters(workflows) + if not validation_result.get("valid", False): + raise ValueError(f"工作流参数验证失败: {validation_result.get('message', '未知错误')}") + + # 构造请求数据 + request_data = { + "name": name, + "workflows": workflows + } + + # 转换为 JSON 字符串 + json_str = json.dumps(request_data, ensure_ascii=False) + + logger.info(f"发送工作流合并请求: {json_str}") + + # 调用底层 API(需要在 bioyond_rpc.py 中实现) + result = self.hardware_interface.merge_workflow_with_parameters(json_str) + + if result.get("code") == 1: + success_msg = f"工作流合并成功: {name}" + logger.info(success_msg) + return { + "success": True, + "code": result.get("code"), + "message": result.get("message", ""), + "timestamp": result.get("timestamp", 0), + "action": "merge_workflow_with_parameters", + "workflow_name": name, + "workflow_count": len(workflows) + } + else: + error_msg = f"工作流合并失败: {result.get('message', '未知错误')}" + logger.error(error_msg) + return { + "success": False, + "code": result.get("code", 0), + "message": result.get("message", error_msg), + "timestamp": result.get("timestamp", 0), + "action": "merge_workflow_with_parameters" + } + + except Exception as e: + error_msg = f"工作流合并操作异常: {str(e)}" + logger.error(error_msg) + traceback.print_exc() + return { + "success": False, + "code": 0, + "message": error_msg, + "action": "merge_workflow_with_parameters" + } + + def validate_workflow_parameters(self, workflows: List[Dict[str, Any]]) -> Dict[str, Any]: + """验证工作流参数格式 + + Args: + workflows (List[Dict[str, Any]]): 工作流列表 + + Returns: + Dict[str, Any]: 验证结果 + """ + # 委托给 RPC 层进行参数验证 + return self.hardware_interface.validate_workflow_parameters(workflows) + + def get_workflow_parameter_template(self) -> Dict[str, Any]: + """获取工作流参数模板 + + Returns: + Dict[str, Any]: 参数模板和说明 + """ + # 委托给 RPC 层获取参数模板 + return self.hardware_interface.get_workflow_parameter_template() + + # ==================== 反应站动作函数 ==================== + # 基于 bioyond_rpc.py 中的反应站方法实现 + + def reactor_taken_out(self, order_id: str = "", preintake_id: str = "", **kwargs) -> Dict[str, Any]: + """反应器取出操作 - 调用底层 order_takeout API + + 从反应站中取出反应器,通过订单ID和预取样ID进行精确控制 + + Args: + order_id (str): 订单ID,用于标识要取出的订单 + preintake_id (str): 预取样ID,用于标识具体的取样任务 + + Returns: + Dict[str, Any]: 操作结果,包含 code 和 return_info + """ + try: + logger.info(f"执行反应器取出操作: 订单ID={order_id}, 预取样ID={preintake_id}") + + # 构造 JSON 参数 + params = { + "order_id": order_id, + "preintake_id": preintake_id + } + json_str = json.dumps(params) + + # 调用底层 order_takeout API + result_code = self.hardware_interface.order_takeout(json_str) + + if result_code == 1: + success_msg = f"反应器取出操作成功完成,订单ID: {order_id}" + logger.info(success_msg) + return { + "success": True, + "code": result_code, + "return_info": success_msg, + "action": "reactor_taken_out" + } + else: + error_msg = f"反应器取出操作失败,返回代码: {result_code}" + logger.error(error_msg) + return { + "success": False, + "code": result_code, + "return_info": error_msg, + "action": "reactor_taken_out" + } + + except Exception as e: + error_msg = f"反应器取出操作异常: {str(e)}" + logger.error(error_msg) + return { + "success": False, + "code": 0, + "return_info": error_msg, + "action": "reactor_taken_out" + } + + def reactor_taken_in(self, **kwargs) -> Dict[str, Any]: + """反应器放入操作 + + 将反应器放入反应站 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info("执行反应器放入操作") + + # 调用 bioyond_rpc.py 中的反应站方法 + result = self.hardware_interface.reactor_taken_in() + + return { + "success": True, + "message": "反应器放入操作完成", + "result": result, + "action": "reactor_taken_in" + } + + except Exception as e: + logger.error(f"反应器放入操作失败: {e}") + return { + "success": False, + "error": str(e), + "action": "reactor_taken_in" + } + + def solid_feeding_vials(self, material_name: str = "", volume: str = "", **kwargs) -> Dict[str, Any]: + """固体进料到小瓶 + + Args: + material_name (str): 物料名称 + volume (str): 进料体积 + **kwargs: 其他参数 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info(f"执行固体进料操作: 物料={material_name}, 体积={volume}") + + # 参数验证 + if not material_name: + raise ValueError("物料名称不能为空") + + # 调用 bioyond_rpc.py 中的反应站方法 + result = self.hardware_interface.solid_feeding_vials( + assign_material_name=material_name, + volume=volume, + **kwargs + ) + + return { + "success": True, + "message": f"固体进料操作完成: {material_name}", + "result": result, + "action": "solid_feeding_vials", + "parameters": { + "material_name": material_name, + "volume": volume + } + } + + except Exception as e: + logger.error(f"固体进料操作失败: {e}") + return { + "success": False, + "error": str(e), + "action": "solid_feeding_vials", + "parameters": { + "material_name": material_name, + "volume": volume + } + } + + def liquid_feeding_vials_non_titration(self, material_name: str = "", volume: str = "", **kwargs) -> Dict[str, Any]: + """非滴定液体进料到小瓶 + + Args: + material_name (str): 物料名称 + volume (str): 进料体积 + **kwargs: 其他参数 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info(f"执行非滴定液体进料操作: 物料={material_name}, 体积={volume}") + + # 参数验证 + if not material_name: + raise ValueError("物料名称不能为空") + if not volume: + raise ValueError("进料体积不能为空") + + # 调用 bioyond_rpc.py 中的反应站方法 + result = self.hardware_interface.liquid_feeding_vials_non_titration( + assign_material_name=material_name, + volume=volume, + **kwargs + ) + + return { + "success": True, + "message": f"非滴定液体进料操作完成: {material_name}", + "result": result, + "action": "liquid_feeding_vials_non_titration", + "parameters": { + "material_name": material_name, + "volume": volume + } + } + + except Exception as e: + logger.error(f"非滴定液体进料操作失败: {e}") + return { + "success": False, + "error": str(e), + "action": "liquid_feeding_vials_non_titration", + "parameters": { + "material_name": material_name, + "volume": volume + } + } + + def liquid_feeding_solvents(self, material_name: str = "", volume: str = "", **kwargs) -> Dict[str, Any]: + """溶剂进料操作 + + Args: + material_name (str): 溶剂名称 + volume (str): 进料体积 + **kwargs: 其他参数 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info(f"执行溶剂进料操作: 溶剂={material_name}, 体积={volume}") + + # 参数验证 + if not material_name: + raise ValueError("溶剂名称不能为空") + if not volume: + raise ValueError("进料体积不能为空") + + # 调用 bioyond_rpc.py 中的反应站方法 + result = self.hardware_interface.liquid_feeding_solvents( + assign_material_name=material_name, + volume=volume, + **kwargs + ) + + return { + "success": True, + "message": f"溶剂进料操作完成: {material_name}", + "result": result, + "action": "liquid_feeding_solvents", + "parameters": { + "material_name": material_name, + "volume": volume + } + } + + except Exception as e: + logger.error(f"溶剂进料操作失败: {e}") + return { + "success": False, + "error": str(e), + "action": "liquid_feeding_solvents", + "parameters": { + "material_name": material_name, + "volume": volume + } + } + + def liquid_feeding_titration(self, material_name: str = "", volume: str = "", + titration_type: str = "1", time: str = "120", + torque_variation: str = "2", **kwargs) -> Dict[str, Any]: + """滴定液体进料操作 + + Args: + material_name (str): 物料名称 + volume (str): 进料体积 + titration_type (str): 滴定类型,默认为"1" + time (str): 滴定时间,默认为"120"秒 + torque_variation (str): 扭矩变化,默认为"2" + **kwargs: 其他参数 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info(f"执行滴定液体进料操作: 物料={material_name}, 体积={volume}, 类型={titration_type}") + + # 参数验证 + if not material_name: + raise ValueError("物料名称不能为空") + if not volume: + raise ValueError("进料体积不能为空") + + # 调用 bioyond_rpc.py 中的反应站方法 + result = self.hardware_interface.liquid_feeding_titration( + assign_material_name=material_name, + volume=volume, + titration_type=titration_type, + time=time, + torque_variation=torque_variation, + **kwargs + ) + + return { + "success": True, + "message": f"滴定液体进料操作完成: {material_name}", + "result": result, + "action": "liquid_feeding_titration", + "parameters": { + "material_name": material_name, + "volume": volume, + "titration_type": titration_type, + "time": time, + "torque_variation": torque_variation + } + } + + except Exception as e: + logger.error(f"滴定液体进料操作失败: {e}") + return { + "success": False, + "error": str(e), + "action": "liquid_feeding_titration", + "parameters": { + "material_name": material_name, + "volume": volume, + "titration_type": titration_type, + "time": time, + "torque_variation": torque_variation + } + } + + def liquid_feeding_beaker(self, material_name: str = "", volume: str = "", **kwargs) -> Dict[str, Any]: + """烧杯液体进料操作 + + Args: + material_name (str): 物料名称 + volume (str): 进料体积 + **kwargs: 其他参数 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info(f"执行烧杯液体进料操作: 物料={material_name}, 体积={volume}") + + # 参数验证 + if not material_name: + raise ValueError("物料名称不能为空") + if not volume: + raise ValueError("进料体积不能为空") + + # 调用 bioyond_rpc.py 中的反应站方法 + result = self.hardware_interface.liquid_feeding_beaker( + assign_material_name=material_name, + volume=volume, + **kwargs + ) + + return { + "success": True, + "message": f"烧杯液体进料操作完成: {material_name}", + "result": result, + "action": "liquid_feeding_beaker", + "parameters": { + "material_name": material_name, + "volume": volume + } + } + + except Exception as e: + logger.error(f"烧杯液体进料操作失败: {e}") + return { + "success": False, + "error": str(e), + "action": "liquid_feeding_beaker", + "parameters": { + "material_name": material_name, + "volume": volume + } + } + + # ==================== 配液站动作函数 ==================== + # 基于 dispensing_station_bioyong.py 中的配液站方法实现 + + def create_order(self, order_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """创建配液任务订单 + + Args: + order_data (Union[str, Dict[str, Any]]): 订单数据,可以是JSON字符串或字典 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info("创建配液任务订单") + + # 处理输入数据 + if isinstance(order_data, str): + order_json = order_data + else: + order_json = json.dumps(order_data) + + # 调用配液站接口 + result = self.dispensing_interface.create_order(order_json) + + return { + "success": True, + "message": "配液任务订单创建完成", + "result": result, + "action": "create_order" + } + + except Exception as e: + logger.error(f"创建配液任务订单失败: {e}") + return { + "success": False, + "error": str(e), + "action": "create_order" + } + + def order_query(self, query_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """查询配液任务状态 + + Args: + query_data (Union[str, Dict[str, Any]]): 查询数据,可以是JSON字符串或字典 + + Returns: + Dict[str, Any]: 查询结果 + """ + try: + logger.info("查询配液任务状态") + + # 处理输入数据 + if isinstance(query_data, str): + query_json = query_data + else: + query_json = json.dumps(query_data) + + # 调用配液站接口 + result = self.dispensing_interface.order_query(query_json) + + return { + "success": True, + "message": "配液任务状态查询完成", + "result": result, + "action": "order_query" + } + + except Exception as e: + logger.error(f"查询配液任务状态失败: {e}") + return { + "success": False, + "error": str(e), + "action": "order_query" + } + + def dispensing_material_inbound(self, material_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """配液站物料入库 + + Args: + material_data (Union[str, Dict[str, Any]]): 物料数据,可以是JSON字符串或字典 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查配液站接口是否可用 + self._check_interface_availability("dispensing") + + logger.info("执行配液站物料入库操作") + + # 处理输入数据 + if isinstance(material_data, str): + material_json = material_data + else: + material_json = json.dumps(material_data) + + # 调用配液站接口 + result = self.dispensing_interface.material_inbound(material_json) + + return { + "success": True, + "message": "配液站物料入库完成", + "result": result, + "action": "dispensing_material_inbound" + } + + except Exception as e: + logger.error(f"配液站物料入库失败: {e}") + return { + "success": False, + "error": str(e), + "action": "dispensing_material_inbound" + } + + def dispensing_material_outbound(self, material_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """配液站物料出库 + + Args: + material_data (Union[str, Dict[str, Any]]): 物料数据,可以是JSON字符串或字典 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查配液站接口是否可用 + self._check_interface_availability("dispensing") + + logger.info("执行配液站物料出库操作") + + # 处理输入数据 + if isinstance(material_data, str): + material_json = material_data + else: + material_json = json.dumps(material_data) + + # 调用配液站接口 + result = self.dispensing_interface.material_outbound(material_json) + + return { + "success": True, + "message": "配液站物料出库完成", + "result": result, + "action": "dispensing_material_outbound" + } + + except Exception as e: + logger.error(f"配液站物料出库失败: {e}") + return { + "success": False, + "error": str(e), + "action": "dispensing_material_outbound" + } + + def delete_material(self, material_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """删除物料 + + Args: + material_data (Union[str, Dict[str, Any]]): 物料数据,可以是JSON字符串或字典 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info("执行删除物料操作") + + # 处理输入数据 + if isinstance(material_data, str): + material_json = material_data + else: + material_json = json.dumps(material_data) + + # 调用配液站接口 + result = self.dispensing_interface.delete_material(material_json) + + return { + "success": True, + "message": "删除物料操作完成", + "result": result, + "action": "delete_material" + } + + except Exception as e: + logger.error(f"删除物料操作失败: {e}") + return { + "success": False, + "error": str(e), + "action": "delete_material" + } + + def sample_waste_removal(self, waste_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """样品废料移除 + + Args: + waste_data (Union[str, Dict[str, Any]]): 废料数据,可以是JSON字符串或字典 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查配液站接口是否可用 + self._check_interface_availability("dispensing") + + logger.info("执行样品废料移除操作") + + # 处理输入数据 + if isinstance(waste_data, str): + waste_json = waste_data + else: + waste_json = json.dumps(waste_data) + + # 调用配液站接口 + result = self.dispensing_interface.sample_waste_removal(waste_json) + + return { + "success": True, + "message": "样品废料移除操作完成", + "result": result, + "action": "sample_waste_removal" + } + + except Exception as e: + logger.error(f"样品废料移除操作失败: {e}") + return { + "success": False, + "error": str(e), + "action": "sample_waste_removal" + } + + def create_resource(self, resource_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """创建资源(样品板等) + + Args: + resource_data (Union[str, Dict[str, Any]]): 资源数据,可以是JSON字符串或字典 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查配液站接口是否可用 + self._check_interface_availability("dispensing") + + logger.info("执行创建资源操作") + + # 处理输入数据 + if isinstance(resource_data, str): + resource_json = resource_data + else: + resource_json = json.dumps(resource_data) + + # 调用配液站接口 + result = self.dispensing_interface.create_resource(resource_json) + + return { + "success": True, + "message": "创建资源操作完成", + "result": result, + "action": "create_resource" + } + + except Exception as e: + logger.error(f"创建资源操作失败: {e}") + return { + "success": False, + "error": str(e), + "action": "create_resource" + } + + def create_90_10_vial_feeding_task(self, task_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """创建90/10比例进料任务 + + Args: + task_data (Union[str, Dict[str, Any]]): 任务数据,可以是JSON字符串或字典 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查配液站接口是否可用 + self._check_interface_availability("dispensing") + + logger.info("创建90/10比例进料任务") + + # 处理输入数据 + if isinstance(task_data, str): + task_params = json.loads(task_data) + else: + task_params = task_data + + # 调用配液站接口,传递具体参数而不是JSON字符串 + result = self.dispensing_interface.create_90_10_vial_feeding_task( + order_name=task_params.get("order_name"), + speed=task_params.get("speed"), + temperature=task_params.get("temperature"), + delay_time=task_params.get("delay_time"), + percent_90_1_assign_material_name=task_params.get("percent_90_1_assign_material_name"), + percent_90_1_target_weigh=task_params.get("percent_90_1_target_weigh"), + percent_90_2_assign_material_name=task_params.get("percent_90_2_assign_material_name"), + percent_90_2_target_weigh=task_params.get("percent_90_2_target_weigh"), + percent_90_3_assign_material_name=task_params.get("percent_90_3_assign_material_name"), + percent_90_3_target_weigh=task_params.get("percent_90_3_target_weigh"), + percent_10_1_assign_material_name=task_params.get("percent_10_1_assign_material_name"), + percent_10_1_target_weigh=task_params.get("percent_10_1_target_weigh"), + percent_10_1_volume=task_params.get("percent_10_1_volume"), + percent_10_1_liquid_material_name=task_params.get("percent_10_1_liquid_material_name"), + percent_10_2_assign_material_name=task_params.get("percent_10_2_assign_material_name"), + percent_10_2_target_weigh=task_params.get("percent_10_2_target_weigh"), + percent_10_2_volume=task_params.get("percent_10_2_volume"), + percent_10_2_liquid_material_name=task_params.get("percent_10_2_liquid_material_name"), + percent_10_3_assign_material_name=task_params.get("percent_10_3_assign_material_name"), + percent_10_3_target_weigh=task_params.get("percent_10_3_target_weigh"), + percent_10_3_volume=task_params.get("percent_10_3_volume"), + percent_10_3_liquid_material_name=task_params.get("percent_10_3_liquid_material_name"), + hold_m_name=task_params.get("hold_m_name") + ) + + return { + "success": True, + "message": "90/10比例进料任务创建完成", + "result": result, + "action": "create_90_10_vial_feeding_task" + } + + except Exception as e: + logger.error(f"创建90/10比例进料任务失败: {e}") + return { + "success": False, + "error": str(e), + "action": "create_90_10_vial_feeding_task" + } + + def create_diamine_solution_task(self, solution_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """创建二胺溶液配制任务 + + Args: + solution_data (Union[str, Dict[str, Any]]): 溶液数据,可以是JSON字符串或字典 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查配液站接口是否可用 + self._check_interface_availability("dispensing") + + logger.info("创建二胺溶液配制任务") + + # 处理输入数据 + if isinstance(solution_data, str): + solution_params = json.loads(solution_data) + else: + solution_params = solution_data + + # 调用配液站接口,传递具体参数而不是JSON字符串 + result = self.dispensing_interface.create_diamine_solution_task( + order_name=solution_params.get("order_name"), + material_name=solution_params.get("material_name"), + target_weigh=solution_params.get("target_weigh"), + volume=solution_params.get("volume"), + liquid_material_name=solution_params.get("liquid_material_name", "NMP"), + speed=solution_params.get("speed"), + temperature=solution_params.get("temperature"), + delay_time=solution_params.get("delay_time"), + hold_m_name=solution_params.get("hold_m_name") + ) + + return { + "success": True, + "message": "二胺溶液配制任务创建完成", + "result": result, + "action": "create_diamine_solution_task" + } + + except Exception as e: + logger.error(f"创建二胺溶液配制任务失败: {e}") + return { + "success": False, + "error": str(e), + "action": "create_diamine_solution_task" + } + + def create_batch_90_10_vial_feeding_task(self, batch_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """ + 创建批量90%10%小瓶投料任务 + + Args: + batch_data: 批量90%10%小瓶投料任务数据,可以是JSON字符串或字典 + 包含batch_name、tasks列表和global_settings + + Returns: + Dict[str, Any]: 批量任务创建结果 + """ + try: + # 检查配液站接口是否可用 + if not self._check_interface_availability("dispensing"): + return { + "success": False, + "error": "配液站接口不可用", + "action": "create_batch_90_10_vial_feeding_task" + } + + # 解析输入数据 + if isinstance(batch_data, str): + batch_params = json.loads(batch_data) + else: + batch_params = batch_data + + logger.info(f"创建批量90%10%小瓶投料任务: {batch_params.get('batch_name', '未命名批量90%10%小瓶投料任务')}") + + # 调用配液站接口的批量90%10%小瓶投料方法 + result = self.dispensing_interface.create_batch_90_10_vial_feeding_task( + json.dumps(batch_params) if isinstance(batch_params, dict) else batch_data + ) + + return { + "success": True, + "result": result, + "action": "create_batch_90_10_vial_feeding_task" + } + + except json.JSONDecodeError as e: + logger.error(f"批量90%10%小瓶投料任务数据解析失败: {e}") + return { + "success": False, + "error": f"JSON解析失败: {str(e)}", + "action": "create_batch_90_10_vial_feeding_task" + } + + except Exception as e: + logger.error(f"创建批量90%10%小瓶投料任务失败: {e}") + return { + "success": False, + "error": str(e), + "action": "create_batch_90_10_vial_feeding_task" + } + + def create_batch_diamine_solution_task(self, batch_data: Union[str, Dict[str, Any]]) -> Dict[str, Any]: + """ + 创建批量二胺溶液配制任务 + + Args: + batch_data: 批量二胺溶液配制任务数据,可以是JSON字符串或字典 + 包含batch_name、tasks列表和global_settings + + Returns: + Dict[str, Any]: 批量任务创建结果 + """ + try: + # 检查配液站接口是否可用 + if not self._check_interface_availability("dispensing"): + return { + "success": False, + "error": "配液站接口不可用", + "action": "create_batch_diamine_solution_task" + } + + # 解析输入数据 + if isinstance(batch_data, str): + batch_params = json.loads(batch_data) + else: + batch_params = batch_data + + logger.info(f"创建批量二胺溶液配制任务: {batch_params.get('batch_name', '未命名批量二胺溶液配制任务')}") + + # 调用配液站接口的批量二胺溶液配制方法 + result = self.dispensing_interface.create_batch_diamine_solution_task( + json.dumps(batch_params) if isinstance(batch_params, dict) else batch_data + ) + + return { + "success": True, + "result": result, + "action": "create_batch_diamine_solution_task" + } + + except json.JSONDecodeError as e: + logger.error(f"批量二胺溶液配制任务数据解析失败: {e}") + return { + "success": False, + "error": f"JSON解析失败: {str(e)}", + "action": "create_batch_diamine_solution_task" + } + + except Exception as e: + logger.error(f"创建批量二胺溶液配制任务失败: {e}") + return { + "success": False, + "error": str(e), + "action": "create_batch_diamine_solution_task" + } + + # ==================== 反应站动作接口 ==================== + + def reaction_station_drip_back(self, volume: str, assign_material_name: str, + time: str, torque_variation: str) -> Dict[str, Any]: + """反应站滴回操作 + + Args: + volume (str): 投料体积 + assign_material_name (str): 溶剂名称 + time (str): 观察时间(单位min) + torque_variation (str): 是否观察1否2是 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查反应站接口是否可用 + self._check_interface_availability("reaction") + + logger.info(f"执行反应站滴回操作: 体积={volume}, 溶剂={assign_material_name}") + + # 调用硬件接口的滴回方法 + result = self.hardware_interface.reactor_taken_out( + volume=volume, + assign_material_name=assign_material_name, + time=time, + torque_variation=torque_variation + ) + + return { + "success": True, + "return_info": "滴回操作完成", + "result": result, + "action": "reaction_station_drip_back" + } + + except Exception as e: + logger.error(f"反应站滴回操作失败: {e}") + return { + "success": False, + "return_info": f"滴回操作失败: {str(e)}", + "action": "reaction_station_drip_back" + } + + def reaction_station_liquid_feed(self, titration_type: str, volume: str, + assign_material_name: str, time: str, + torque_variation: str) -> Dict[str, Any]: + """反应站液体投料操作 + + Args: + titration_type (str): 滴定类型1否2是 + volume (str): 投料体积 + assign_material_name (str): 溶剂名称 + time (str): 观察时间(单位min) + torque_variation (str): 是否观察1否2是 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查反应站接口是否可用 + self._check_interface_availability("reaction") + + logger.info(f"执行反应站液体投料: 类型={titration_type}, 体积={volume}, 溶剂={assign_material_name}") + + # 根据滴定类型选择相应的方法 + if titration_type == "2": # 滴定 + result = self.hardware_interface.liquid_feeding_titration( + volume=volume, + assign_material_name=assign_material_name, + time=time, + torque_variation=torque_variation + ) + else: # 非滴定 + result = self.hardware_interface.liquid_feeding_vials_non_titration( + volume=volume, + assign_material_name=assign_material_name, + time=time, + torque_variation=torque_variation + ) + + return { + "success": True, + "return_info": "液体投料操作完成", + "result": result, + "action": "reaction_station_liquid_feed" + } + + except Exception as e: + logger.error(f"反应站液体投料操作失败: {e}") + return { + "success": False, + "return_info": f"液体投料操作失败: {str(e)}", + "action": "reaction_station_liquid_feed" + } + + def reaction_station_solid_feed_vial(self, assign_material_name: str, material_id: str, + time: str, torque_variation: str) -> Dict[str, Any]: + """反应站固体投料-小瓶操作 + + Args: + assign_material_name (str): 固体名称_粉末加样模块-投料 + material_id (str): 固体投料类型_粉末加样模块-投料 + time (str): 观察时间_反应模块-观察搅拌结果 + torque_variation (str): 是否观察1否2是_反应模块-观察搅拌结果 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查反应站接口是否可用 + self._check_interface_availability("reaction") + + logger.info(f"执行反应站固体投料: 固体={assign_material_name}, ID={material_id}") + + # 调用硬件接口的固体投料方法 + result = self.hardware_interface.solid_feeding_vials( + assign_material_name=assign_material_name, + material_id=material_id, + time=time, + torque_variation=torque_variation + ) + + return { + "success": True, + "return_info": "固体投料操作完成", + "result": result, + "action": "reaction_station_solid_feed_vial" + } + + except Exception as e: + logger.error(f"反应站固体投料操作失败: {e}") + return { + "success": False, + "return_info": f"固体投料操作失败: {str(e)}", + "action": "reaction_station_solid_feed_vial" + } + + def reaction_station_take_in(self, cutoff: str, temperature: str, + assign_material_name: str) -> Dict[str, Any]: + """反应站取入操作 + + Args: + cutoff (str): 截止参数 + temperature (str): 温度 + assign_material_name (str): 物料名称 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查反应站接口是否可用 + self._check_interface_availability("reaction") + + logger.info(f"执行反应站取入操作: 温度={temperature}, 物料={assign_material_name}") + + # 调用硬件接口的取入方法 + result = self.hardware_interface.reactor_taken_in( + cutoff=cutoff, + temperature=temperature, + assign_material_name=assign_material_name + ) + + return { + "success": True, + "return_info": "取入操作完成", + "result": result, + "action": "reaction_station_take_in" + } + + except Exception as e: + logger.error(f"反应站取入操作失败: {e}") + return { + "success": False, + "return_info": f"取入操作失败: {str(e)}", + "action": "reaction_station_take_in" + } + + def reaction_station_reactor_taken_out(self, order_id: str = "", preintake_id: str = "") -> Dict[str, Any]: + """反应站反应器取出操作 + + Args: + order_id (str): 订单ID,用于标识要取出的订单 + preintake_id (str): 预取样ID,用于标识具体的取样任务 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查反应站接口是否可用 + self._check_interface_availability("reaction") + + logger.info(f"执行反应站反应器取出操作: 订单ID={order_id}, 预取样ID={preintake_id}") + + # 调用更新后的反应器取出方法 + result = self.reactor_taken_out(order_id=order_id, preintake_id=preintake_id) + + # 更新 action 字段以区分调用来源 + result["action"] = "reaction_station_reactor_taken_out" + + return result + + except Exception as e: + logger.error(f"反应站反应器取出操作失败: {e}") + return { + "success": False, + "code": 0, + "return_info": f"反应器取出操作失败: {str(e)}", + "action": "reaction_station_reactor_taken_out" + } + + def reaction_station_process_execute(self, workflow_name: str, task_name: str) -> Dict[str, Any]: + """反应站流程执行操作 + + Args: + workflow_name (str): 工作流名称 + task_name (str): 任务名称 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + # 检查反应站接口是否可用 + self._check_interface_availability("reaction") + + logger.info(f"执行反应站流程: 工作流={workflow_name}, 任务={task_name}") + + # 这里可以根据具体的工作流和任务名称调用相应的方法 + # 暂时使用通用的执行方法 + result = { + "workflow_name": workflow_name, + "task_name": task_name, + "status": "executed" + } + + return { + "success": True, + "return_info": "流程执行完成", + "result": result, + "action": "reaction_station_process_execute" + } + + except Exception as e: + logger.error(f"反应站流程执行失败: {e}") + return { + "success": False, + "return_info": f"流程执行失败: {str(e)}", + "action": "reaction_station_process_execute" + } + + # ==================== 物料管理动作函数 ==================== + + def material_inbound(self, material_id: str, location_name: str) -> Dict[str, Any]: + """物料入库操作 + + 将物料添加到指定位置 + + Args: + material_id (str): 物料ID + location_name (str): 位置名称 + + Returns: + Dict[str, Any]: 操作结果,包含状态和消息 + """ + try: + logger.info(f"开始执行物料入库操作: 物料ID={material_id}, 位置={location_name}") + result = self.hardware_interface.material_inbound( + material_id=material_id, + location_name=location_name + ) + + if result: + logger.info("物料入库操作成功") + return { + "status": "success", + "message": f"物料入库成功,物料ID: {material_id}", + "data": result + } + else: + logger.error("物料入库操作失败") + return { + "status": "failed", + "message": "物料入库失败" + } + + except Exception as e: + logger.error(f"物料入库操作异常: {e}") + return { + "status": "error", + "message": f"物料入库操作异常: {str(e)}" + } + + def material_outbound(self, material_id: str, location_name: str, + quantity: int) -> Dict[str, Any]: + """物料出库操作 + + 从指定位置取出物料 + + Args: + material_id (str): 物料ID + location_name (str): 位置名称 + quantity (int): 数量 + + Returns: + Dict[str, Any]: 操作结果,包含状态和消息 + """ + try: + logger.info(f"开始执行物料出库操作: 物料ID={material_id}, 位置={location_name}, 数量={quantity}") + result = self.hardware_interface.material_outbound( + material_id=material_id, + location_name=location_name, + quantity=quantity + ) + + if result: + logger.info("物料出库操作成功") + return { + "status": "success", + "message": f"物料出库成功,物料ID: {material_id}", + "data": result + } + else: + logger.error("物料出库操作失败") + return { + "status": "failed", + "message": "物料出库失败" + } + + except Exception as e: + logger.error(f"物料出库操作异常: {e}") + return { + "status": "error", + "message": f"物料出库操作异常: {str(e)}" + } + + # ============ 工作流控制函数 ============ + + def create_order(self, workflow_name: str, task_name: str, + parameters: Dict[str, Any] = None) -> Dict[str, Any]: + """创建工作流订单 + + 创建并提交工作流执行订单 + + Args: + workflow_name (str): 工作流名称 + task_name (str): 任务名称 + parameters (Dict[str, Any]): 工作流参数 + + Returns: + Dict[str, Any]: 操作结果,包含状态和订单信息 + """ + try: + logger.info(f"开始创建工作流订单: 工作流={workflow_name}, 任务={task_name}") + + # 使用 BioyondV1RPC 的工作流处理方法 + result = self.hardware_interface.process_and_execute_workflow( + workflow_name=workflow_name, + task_name=task_name + ) + + if result and result.get("status") == "success": + logger.info("工作流订单创建成功") + return { + "status": "success", + "message": f"工作流订单创建成功: {workflow_name}", + "data": result + } + else: + logger.error("工作流订单创建失败") + return { + "status": "failed", + "message": "工作流订单创建失败", + "data": result + } + + except Exception as e: + logger.error(f"创建工作流订单异常: {e}") + return { + "status": "error", + "message": f"创建工作流订单异常: {str(e)}" + } + + def get_scheduler_status(self) -> Dict[str, Any]: + """获取调度器状态 + + Returns: + Dict[str, Any]: 调度器状态信息 + """ + try: + logger.info("获取调度器状态") + result = self.hardware_interface.scheduler_status() + + return { + "status": "success", + "message": "调度器状态获取成功", + "data": result + } + + except Exception as e: + logger.error(f"获取调度器状态异常: {e}") + return { + "status": "error", + "message": f"获取调度器状态异常: {str(e)}" + } + + def start_scheduler(self) -> Dict[str, Any]: + """启动调度器 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info("启动调度器") + result = self.hardware_interface.scheduler_start() + + if result == 1: # 成功返回1 + logger.info("调度器启动成功") + return { + "status": "success", + "message": "调度器启动成功" + } + else: + logger.error("调度器启动失败") + return { + "status": "failed", + "message": "调度器启动失败" + } + + except Exception as e: + logger.error(f"启动调度器异常: {e}") + return { + "status": "error", + "message": f"启动调度器异常: {str(e)}" + } + + def stop_scheduler(self) -> Dict[str, Any]: + """停止调度器 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info("停止调度器") + result = self.hardware_interface.scheduler_stop() + + if result == 1: # 成功返回1 + logger.info("调度器停止成功") + return { + "status": "success", + "message": "调度器停止成功" + } + else: + logger.error("调度器停止失败") + return { + "status": "failed", + "message": "调度器停止失败" + } + + except Exception as e: + logger.error(f"停止调度器异常: {e}") + return { + "status": "error", + "message": f"停止调度器异常: {str(e)}" + } + + # ============ 其他操作函数 ============ + + def drip_back(self, assign_material_name: str = "Reactor", time: str = "0", + torque_variation: str = "1", temperature: float = 25.00) -> Dict[str, Any]: + """滴回操作 + + 执行滴回操作,通常用于反应后的物料回收 + + Args: + assign_material_name (str): 指定的物料名称,默认为 "Reactor" + time (str): 操作时间,默认为 "0" + torque_variation (str): 扭矩变化,默认为 "1" + temperature (float): 温度设置,默认为 25.00°C + + Returns: + Dict[str, Any]: 操作结果,包含状态和消息 + """ + try: + logger.info(f"开始执行滴回操作: 物料={assign_material_name}, 温度={temperature}°C") + + # 根据配置文件中的映射,滴回操作可能对应特定的工作流 + workflow_name = self.config.get("workflow_mappings", {}).get("Drip_back") + + if workflow_name: + result = self.hardware_interface.process_and_execute_workflow( + workflow_name=workflow_name, + task_name="drip_back_task" + ) + else: + # 如果没有特定的工作流映射,使用通用的液体操作 + logger.warning("未找到滴回操作的工作流映射,使用默认处理") + result = {"status": "success", "message": "滴回操作完成"} + + if result and result.get("status") == "success": + logger.info("滴回操作成功") + return { + "status": "success", + "message": f"滴回操作成功,物料: {assign_material_name}", + "data": result + } + else: + logger.error("滴回操作失败") + return { + "status": "failed", + "message": "滴回操作失败" + } + + except Exception as e: + logger.error(f"滴回操作异常: {e}") + return { + "status": "error", + "message": f"滴回操作异常: {str(e)}" + } + + def get_device_list(self) -> Dict[str, Any]: + """获取设备列表 + + Returns: + Dict[str, Any]: 设备列表信息 + """ + try: + logger.info("获取设备列表") + result = self.hardware_interface.device_list() + + return { + "status": "success", + "message": "设备列表获取成功", + "data": result + } + + except Exception as e: + logger.error(f"获取设备列表异常: {e}") + return { + "status": "error", + "message": f"获取设备列表异常: {str(e)}" + } + + def device_operation(self, device_id: str, operation: str, + parameters: Dict[str, Any] = None) -> Dict[str, Any]: + """设备操作 + + 对指定设备执行操作 + + Args: + device_id (str): 设备ID + operation (str): 操作类型 + parameters (Dict[str, Any]): 操作参数 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info(f"执行设备操作: 设备ID={device_id}, 操作={operation}") + result = self.hardware_interface.device_operation( + device_id=device_id, + operation=operation, + parameters=parameters or {} + ) + + if result: + logger.info("设备操作成功") + return { + "status": "success", + "message": f"设备操作成功: {operation}", + "data": result + } + else: + logger.error("设备操作失败") + return { + "status": "failed", + "message": "设备操作失败" + } + + except Exception as e: + logger.error(f"设备操作异常: {e}") + return { + "status": "error", + "message": f"设备操作异常: {str(e)}" + } + + def add_material(self, material_data: Dict[str, Any]) -> Dict[str, Any]: + """添加物料 + + 向系统中添加新的物料信息 + + Args: + material_data (Dict[str, Any]): 物料数据 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info(f"添加物料: {material_data.get('name', 'Unknown')}") + result = self.hardware_interface.add_material(material_data) + + if result: + logger.info("物料添加成功") + return { + "status": "success", + "message": "物料添加成功", + "data": result + } + else: + logger.error("物料添加失败") + return { + "status": "failed", + "message": "物料添加失败" + } + + except Exception as e: + logger.error(f"添加物料异常: {e}") + return { + "status": "error", + "message": f"添加物料异常: {str(e)}" + } + + def stock_material(self, material_id: str, location: str, + quantity: int) -> Dict[str, Any]: + """库存物料 + + 更新物料库存信息 + + Args: + material_id (str): 物料ID + location (str): 位置 + quantity (int): 数量 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info(f"更新物料库存: 物料ID={material_id}, 位置={location}, 数量={quantity}") + result = self.hardware_interface.stock_material( + material_id=material_id, + location=location, + quantity=quantity + ) + + if result: + logger.info("物料库存更新成功") + return { + "status": "success", + "message": "物料库存更新成功", + "data": result + } + else: + logger.error("物料库存更新失败") + return { + "status": "failed", + "message": "物料库存更新失败" + } + + except Exception as e: + logger.error(f"物料库存更新异常: {e}") + return { + "status": "error", + "message": f"物料库存更新异常: {str(e)}" + } + + # ============ 工作站状态管理 ============ + + def get_workstation_status(self) -> Dict[str, Any]: + """获取工作站状态 + + Returns: + Dict[str, Any]: 工作站状态信息 + """ + try: + # 获取基础状态信息 + base_status = { + "is_busy": self.is_busy, + "workflow_status": self.workflow_status, + "workflow_runtime": self.workflow_runtime + } + + # 获取调度器状态 + scheduler_status = self.get_scheduler_status() + + # 获取设备列表 + device_list = self.get_device_list() + + return { + "status": "success", + "message": "工作站状态获取成功", + "data": { + "base_status": base_status, + "scheduler_status": scheduler_status.get("data"), + "device_list": device_list.get("data"), + "config": { + "api_host": self.config.get("api_host"), + "workflow_mappings": self.config.get("workflow_mappings", {}), + "material_type_mappings": self.config.get("material_type_mappings", {}) + } + } + } + + except Exception as e: + logger.error(f"获取工作站状态异常: {e}") + return { + "status": "error", + "message": f"获取工作站状态异常: {str(e)}" + } + + def get_bioyond_status(self) -> Dict[str, Any]: + """获取完整的 Bioyond 状态信息 + + 这个方法提供了比 bioyond_status 属性更详细的状态信息, + 包括错误处理和格式化的响应结构 + + Returns: + Dict[str, Any]: 格式化的 Bioyond 状态响应 + """ + try: + # 获取 bioyond_status 属性的数据 + status_data = self.bioyond_status + + return { + "status": "success", + "message": "Bioyond 状态获取成功", + "data": status_data + } + + except Exception as e: + logger.error(f"获取 Bioyond 状态异常: {e}") + return { + "status": "error", + "message": f"获取 Bioyond 状态异常: {str(e)}", + "data": { + "station_info": { + "station_name": getattr(self, 'station_name', 'BioyondWorkstation'), + "station_type": getattr(self, 'station_type', 'unknown'), + "enable_reaction_station": getattr(self, 'enable_reaction_station', False), + "enable_dispensing_station": getattr(self, 'enable_dispensing_station', False) + }, + "interface_status": { + "reaction_interface_connected": False, + "dispensing_interface_connected": False, + "last_sync_time": 0, + "sync_interval": 60 + }, + "sync_status": { + "last_sync_success": False, + "total_resources": 0, + "warehouse_count": 0 + }, + "timestamp": __import__('time').time(), + "status": "error", + "error_message": str(e) + } + } + + def reset_workstation(self) -> Dict[str, Any]: + """重置工作站 + + 重置工作站到初始状态 + + Returns: + Dict[str, Any]: 操作结果 + """ + try: + logger.info("开始重置工作站") + + # 停止当前工作流(如果有) + if self.is_busy: + self.stop_workflow() + + # 停止调度器 + self.stop_scheduler() + + # 重新启动调度器 + start_result = self.start_scheduler() + + if start_result.get("status") == "success": + logger.info("工作站重置成功") + return { + "status": "success", + "message": "工作站重置成功" + } + else: + logger.error("工作站重置失败") + return { + "status": "failed", + "message": "工作站重置失败" + } + + except Exception as e: + logger.error(f"工作站重置异常: {e}") + return { + "status": "error", + "message": f"工作站重置异常: {str(e)}" + } + async def execute_bioyond_update_workflow(self, parameters: Dict[str, Any]) -> Dict[str, Any]: """执行Bioyond更新工作流""" try: material_ids = parameters.get("material_ids", []) sync_all = parameters.get("sync_all", True) - + logger.info(f"开始执行Bioyond更新工作流: sync_all={sync_all}") - + # 获取物料管理模块 material_manager = self.material_management - + if sync_all: # 同步所有物料 success_count = 0 @@ -242,46 +2221,46 @@ class BioyondWorkstation(WorkstationBase): success = await material_manager.sync_to_bioyond(resource) if success: success_count += 1 - + result = { "status": "success", "message": f"Bioyond更新完成", "updated_resources": success_count, "total_resources": len(material_ids) if not sync_all else len(material_manager.plr_resources) } - + logger.info(f"Bioyond更新工作流执行完成: {result['status']}") return result - + except Exception as e: logger.error(f"Bioyond更新工作流执行失败: {e}") return { "status": "error", "message": str(e) } - + def load_bioyond_data_from_file(self, file_path: str) -> bool: """从文件加载Bioyond数据(用于测试)""" try: with open(file_path, 'r', encoding='utf-8') as f: bioyond_data = json.load(f) - + # 获取物料管理模块 material_manager = self.material_management - + # 转换为UniLab格式 if isinstance(bioyond_data, dict) and "data" in bioyond_data: unilab_resources = material_manager.resource_bioyond_container_to_ulab(bioyond_data) else: unilab_resources = material_manager.resource_bioyond_to_ulab(bioyond_data) - + # 分配到Deck import asyncio asyncio.create_task(material_manager._assign_resources_to_deck(unilab_resources)) - + logger.info(f"从文件 {file_path} 加载了 {len(unilab_resources)} 个Bioyond资源") return True - + except Exception as e: logger.error(f"从文件加载Bioyond数据失败: {e}") return False @@ -290,10 +2269,10 @@ class BioyondWorkstation(WorkstationBase): # 使用示例 def create_bioyond_workstation_example(): """创建Bioyond工作站示例""" - + # 配置参数 device_id = "bioyond_workstation_001" - + # 子资源配置 children = { "plate_1": { @@ -308,7 +2287,7 @@ def create_bioyond_workstation_example(): } } } - + # Bioyond配置 bioyond_config = { "base_url": "http://bioyond.example.com/api", @@ -316,7 +2295,7 @@ def create_bioyond_workstation_example(): "sync_interval": 60, # 60秒同步一次 "timeout": 30 } - + # Deck配置 deck_config = { "size_x": 1000.0, @@ -324,30 +2303,30 @@ def create_bioyond_workstation_example(): "size_z": 100.0, "model": "BioyondDeck" } - + # 创建工作站 workstation = BioyondWorkstation( station_resource=deck_config, bioyond_config=bioyond_config, deck_config=deck_config, ) - + return workstation if __name__ == "__main__": # 创建示例工作站 #workstation = create_bioyond_workstation_example() - + # 从文件加载测试数据 #workstation.load_bioyond_data_from_file("bioyond_test_yibin.json") - + # 获取状态 #status = workstation.get_bioyond_status() #print("Bioyond工作站状态:", status) # 创建测试数据 - 使用resource_bioyond_container_to_ulab函数期望的格式 - + # 读取 bioyond_resources_unilab_output3 copy.json 文件 from unilabos.resources.graphio import resource_ulab_to_plr, convert_resources_to_type from Bioyond_wuliao import * @@ -356,7 +2335,7 @@ if __name__ == "__main__": import json from pylabrobot.resources.deck import Deck from pylabrobot.resources.coordinate import Coordinate - + with open("./bioyond_test_yibin3_unilab_result_corr.json", "r", encoding="utf-8") as f: bioyond_resources_unilab = json.load(f) print(f"成功读取 JSON 文件,包含 {len(bioyond_resources_unilab)} 个资源") @@ -392,9 +2371,9 @@ if __name__ == "__main__": resources = convert_resources_from_type([deck], [PLRResource]) - + print(resources) http_client.remote_addr = "https://uni-lab.bohrium.com/api/v1" #http_client.auth = "9F05593C" http_client.auth = "ED634D1C" - http_client.resource_add(resources, database_process_later=False) \ No newline at end of file + http_client.resource_add(resources, database_process_later=False) diff --git a/unilabos/registry/devices/dispensing_station_bioyond.yaml b/unilabos/registry/devices/dispensing_station_bioyond.yaml new file mode 100644 index 00000000..4c87e962 --- /dev/null +++ b/unilabos/registry/devices/dispensing_station_bioyond.yaml @@ -0,0 +1,506 @@ +dispensing_station.bioyond: + category: + - work_station + - dispensing_station_bioyond + class: + action_value_mappings: + bioyond_sync: + feedback: {} + goal: + force_sync: force_sync + sync_type: sync_type + goal_default: + force_sync: false + sync_type: full + handles: {} + result: {} + schema: + description: 从Bioyond系统同步物料 + properties: + feedback: {} + goal: + properties: + force_sync: + description: 是否强制同步 + type: boolean + sync_type: + description: 同步类型 + enum: + - full + - incremental + type: string + required: + - sync_type + type: object + result: {} + required: + - goal + title: bioyond_sync参数 + type: object + type: UniLabJsonCommand + bioyond_update: + feedback: {} + goal: + material_ids: material_ids + sync_all: sync_all + goal_default: + material_ids: [] + sync_all: true + handles: {} + result: {} + schema: + description: 将本地物料变更同步到Bioyond + properties: + feedback: {} + goal: + properties: + material_ids: + description: 要同步的物料ID列表 + items: + type: string + type: array + sync_all: + description: 是否同步所有物料 + type: boolean + required: + - sync_all + type: object + result: {} + required: + - goal + title: bioyond_update参数 + type: object + type: UniLabJsonCommand + create_90_10_vial_feeding_task: + feedback: {} + goal: + delay_time: delay_time + order_name: order_name + percent_10_1_assign_material_name: percent_10_1_assign_material_name + percent_10_1_liquid_material_name: percent_10_1_liquid_material_name + percent_10_1_target_weigh: percent_10_1_target_weigh + percent_10_1_volume: percent_10_1_volume + percent_10_2_assign_material_name: percent_10_2_assign_material_name + percent_10_2_liquid_material_name: percent_10_2_liquid_material_name + percent_10_2_target_weigh: percent_10_2_target_weigh + percent_10_2_volume: percent_10_2_volume + percent_90_1_assign_material_name: percent_90_1_assign_material_name + percent_90_1_target_weigh: percent_90_1_target_weigh + percent_90_2_assign_material_name: percent_90_2_assign_material_name + percent_90_2_target_weigh: percent_90_2_target_weigh + percent_90_3_assign_material_name: percent_90_3_assign_material_name + percent_90_3_target_weigh: percent_90_3_target_weigh + speed: speed + temperature: temperature + goal_default: + delay_time: '600' + order_name: '' + percent_10_1_assign_material_name: '' + percent_10_1_liquid_material_name: '' + percent_10_1_target_weigh: '' + percent_10_1_volume: '' + percent_10_2_assign_material_name: '' + percent_10_2_liquid_material_name: '' + percent_10_2_target_weigh: '' + percent_10_2_volume: '' + percent_90_1_assign_material_name: '' + percent_90_1_target_weigh: '' + percent_90_2_assign_material_name: '' + percent_90_2_target_weigh: '' + percent_90_3_assign_material_name: '' + percent_90_3_target_weigh: '' + speed: '400' + temperature: '20' + handles: {} + result: {} + schema: + description: 创建90%/10%小瓶投料任务 + properties: + feedback: {} + goal: + properties: + delay_time: + default: '600' + description: 延迟时间(s) + type: string + order_name: + description: 任务名称 + type: string + percent_10_1_assign_material_name: + description: 10%组分1物料名称 + type: string + percent_10_1_liquid_material_name: + description: 10%组分1液体物料名称 + type: string + percent_10_1_target_weigh: + description: 10%组分1目标重量(g) + type: string + percent_10_1_volume: + description: 10%组分1液体体积(mL) + type: string + percent_10_2_assign_material_name: + description: 10%组分2物料名称 + type: string + percent_10_2_liquid_material_name: + description: 10%组分2液体物料名称 + type: string + percent_10_2_target_weigh: + description: 10%组分2目标重量(g) + type: string + percent_10_2_volume: + description: 10%组分2液体体积(mL) + type: string + percent_90_1_assign_material_name: + description: 90%组分1物料名称 + type: string + percent_90_1_target_weigh: + description: 90%组分1目标重量(g) + type: string + percent_90_2_assign_material_name: + description: 90%组分2物料名称 + type: string + percent_90_2_target_weigh: + description: 90%组分2目标重量(g) + type: string + percent_90_3_assign_material_name: + description: 90%组分3物料名称 + type: string + percent_90_3_target_weigh: + description: 90%组分3目标重量(g) + type: string + speed: + default: '400' + description: 搅拌速度(rpm) + type: string + temperature: + default: '20' + description: 温度(°C) + type: string + type: object + result: {} + required: + - goal + title: create_90_10_vial_feeding_task参数 + type: object + type: UniLabJsonCommand + create_batch_90_10_vial_feeding_task: + feedback: {} + goal: + batch_data: batch_data + goal_default: + batch_data: '{}' + handles: {} + result: {} + schema: + description: 创建批量90%10%小瓶投料任务 + properties: + feedback: {} + goal: + properties: + batch_data: + description: 批量90%10%小瓶投料任务数据(JSON格式),包含batch_name、tasks列表和global_settings + type: string + required: + - batch_data + type: object + result: {} + required: + - goal + title: create_batch_90_10_vial_feeding_task参数 + type: object + type: UniLabJsonCommand + create_batch_diamine_solution_task: + feedback: {} + goal: + batch_data: batch_data + goal_default: + batch_data: '{}' + handles: {} + result: {} + schema: + description: 创建批量二胺溶液配制任务 + properties: + feedback: {} + goal: + properties: + batch_data: + description: 批量二胺溶液配制任务数据(JSON格式),包含batch_name、tasks列表和global_settings + type: string + required: + - batch_data + type: object + result: {} + required: + - goal + title: create_batch_diamine_solution_task参数 + type: object + type: UniLabJsonCommand + create_diamine_solution_task: + feedback: {} + goal: + delay_time: delay_time + hold_m_name: hold_m_name + liquid_material_name: liquid_material_name + material_name: material_name + order_name: order_name + speed: speed + target_weigh: target_weigh + temperature: temperature + volume: volume + goal_default: + delay_time: '600' + hold_m_name: '' + liquid_material_name: NMP + material_name: '' + order_name: '' + speed: '400' + target_weigh: '' + temperature: '20' + volume: '' + handles: {} + result: {} + schema: + description: 创建二胺溶液配制任务 + properties: + feedback: {} + goal: + properties: + delay_time: + default: '600' + description: 延迟时间(s) + type: string + hold_m_name: + description: 库位名称(如ODA-1) + type: string + liquid_material_name: + default: NMP + description: 液体物料名称 + type: string + material_name: + description: 固体物料名称 + type: string + order_name: + description: 任务名称 + type: string + speed: + default: '400' + description: 搅拌速度(rpm) + type: string + target_weigh: + description: 固体目标重量(g) + type: string + temperature: + default: '20' + description: 温度(°C) + type: string + volume: + description: 液体体积(mL) + type: string + required: + - material_name + - target_weigh + - volume + type: object + result: {} + required: + - goal + title: create_diamine_solution_task参数 + type: object + type: UniLabJsonCommand + create_resource: + feedback: {} + goal: + resource_config: resource_config + resource_type: resource_type + goal_default: + resource_config: {} + resource_type: '' + handles: {} + result: {} + schema: + description: 创建资源操作 + properties: + feedback: {} + goal: + properties: + resource_config: + description: 资源配置 + type: object + resource_type: + description: 资源类型 + type: string + required: + - resource_type + - resource_config + type: object + result: {} + required: + - goal + title: create_resource参数 + type: object + type: UniLabJsonCommand + dispensing_material_inbound: + feedback: {} + goal: + location: location + material_id: material_id + goal_default: + location: '' + material_id: '' + handles: {} + result: {} + schema: + description: 配液站物料入库操作 + properties: + feedback: {} + goal: + properties: + location: + description: 存储位置 + type: string + material_id: + description: 物料ID + type: string + required: + - material_id + - location + type: object + result: {} + required: + - goal + title: dispensing_material_inbound参数 + type: object + type: UniLabJsonCommand + dispensing_material_outbound: + feedback: {} + goal: + material_id: material_id + quantity: quantity + goal_default: + material_id: '' + quantity: 0.0 + handles: {} + result: {} + schema: + description: 配液站物料出库操作 + properties: + feedback: {} + goal: + properties: + material_id: + description: 物料ID + type: string + quantity: + description: 出库数量 + type: number + required: + - material_id + - quantity + type: object + result: {} + required: + - goal + title: dispensing_material_outbound参数 + type: object + type: UniLabJsonCommand + sample_waste_removal: + feedback: {} + goal: + sample_id: sample_id + waste_type: waste_type + goal_default: + sample_id: '' + waste_type: general + handles: {} + result: {} + schema: + description: 样品废料移除操作 + properties: + feedback: {} + goal: + properties: + sample_id: + description: 样品ID + type: string + waste_type: + description: 废料类型 + enum: + - general + - hazardous + - organic + - inorganic + type: string + required: + - sample_id + type: object + result: {} + required: + - goal + title: sample_waste_removal参数 + type: object + type: UniLabJsonCommand + module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation + protocol_type: [] + status_types: + bioyond_status: dict + enable_dispensing_station: bool + enable_reaction_station: bool + station_type: str + type: python + config_info: [] + description: Bioyond配液站 - 专门用于物料配制和管理的工作站 + handles: [] + icon: 配液站.webp + init_param_schema: + config: + properties: + bioyond_config: + description: Bioyond API配置 + properties: + api_host: + description: Bioyond API主机地址 + type: string + api_key: + description: Bioyond API密钥 + type: string + material_type_mappings: + description: 物料类型映射配置 + type: object + workflow_mappings: + description: 工作流映射配置 + type: object + type: object + deck: + description: Deck配置 + type: object + station_config: + description: 配液站配置 + properties: + description: + description: 配液站描述 + type: string + enable_dispensing_station: + default: true + description: 启用配液站功能 + type: boolean + enable_reaction_station: + default: false + description: 禁用反应站功能 + type: boolean + station_name: + description: 配液站名称 + type: string + station_type: + default: dispensing_station + description: 站点类型 - 配液站 + enum: + - dispensing_station + type: string + type: object + required: [] + type: object + data: + properties: {} + required: [] + type: object + version: 1.0.0 diff --git a/unilabos/registry/devices/reaction_station_bioyond.yaml b/unilabos/registry/devices/reaction_station_bioyond.yaml new file mode 100644 index 00000000..2dc341a6 --- /dev/null +++ b/unilabos/registry/devices/reaction_station_bioyond.yaml @@ -0,0 +1,384 @@ +reaction_station.bioyond: + category: + - work_station + - reaction_station_bioyond + class: + action_value_mappings: + bioyond_sync: + feedback: {} + goal: + force_sync: force_sync + sync_type: sync_type + goal_default: + force_sync: false + sync_type: full + handles: {} + result: {} + schema: + description: 从Bioyond系统同步物料 + properties: + feedback: {} + goal: + properties: + force_sync: + description: 是否强制同步 + type: boolean + sync_type: + description: 同步类型 + enum: + - full + - incremental + type: string + required: + - sync_type + type: object + result: {} + required: + - goal + title: bioyond_sync参数 + type: object + type: UniLabJsonCommand + bioyond_update: + feedback: {} + goal: + material_ids: material_ids + sync_all: sync_all + goal_default: + material_ids: [] + sync_all: true + handles: {} + result: {} + schema: + description: 将本地物料变更同步到Bioyond + properties: + feedback: {} + goal: + properties: + material_ids: + description: 要同步的物料ID列表 + items: + type: string + type: array + sync_all: + description: 是否同步所有物料 + type: boolean + required: + - sync_all + type: object + result: {} + required: + - goal + title: bioyond_update参数 + type: object + type: UniLabJsonCommand + reaction_station_drip_back: + feedback: {} + goal: + assign_material_name: assign_material_name + time: time + torque_variation: torque_variation + volume: volume + goal_default: + assign_material_name: '' + time: '' + torque_variation: '' + volume: '' + handles: {} + result: {} + schema: + description: 反应站滴回操作 + properties: + feedback: {} + goal: + properties: + assign_material_name: + description: 溶剂名称 + type: string + time: + description: 观察时间(单位min) + type: string + torque_variation: + description: 是否观察1否2是 + type: string + volume: + description: 投料体积 + type: string + required: + - volume + - assign_material_name + - time + - torque_variation + type: object + result: {} + required: + - goal + title: reaction_station_drip_back参数 + type: object + type: UniLabJsonCommand + reaction_station_liquid_feed: + feedback: {} + goal: + assign_material_name: assign_material_name + time: time + titration_type: titration_type + torque_variation: torque_variation + volume: volume + goal_default: + assign_material_name: '' + time: '' + titration_type: '' + torque_variation: '' + volume: '' + handles: {} + result: {} + schema: + description: 反应站液体进料操作 + properties: + feedback: {} + goal: + properties: + assign_material_name: + description: 溶剂名称 + type: string + time: + description: 观察时间(单位min) + type: string + titration_type: + description: 滴定类型1否2是 + type: string + torque_variation: + description: 是否观察1否2是 + type: string + volume: + description: 投料体积 + type: string + required: + - titration_type + - volume + - assign_material_name + - time + - torque_variation + type: object + result: {} + required: + - goal + title: reaction_station_liquid_feed参数 + type: object + type: UniLabJsonCommand + reaction_station_process_execute: + feedback: {} + goal: + task_name: task_name + workflow_name: workflow_name + goal_default: + task_name: '' + workflow_name: '' + handles: {} + result: {} + schema: + description: 反应站流程执行 + properties: + feedback: {} + goal: + properties: + task_name: + description: 任务名称 + type: string + workflow_name: + description: 工作流名称 + type: string + required: + - workflow_name + - task_name + type: object + result: {} + required: + - goal + title: reaction_station_process_execute参数 + type: object + type: UniLabJsonCommand + reaction_station_reactor_taken_out: + feedback: {} + goal: + order_id: order_id + preintake_id: preintake_id + goal_default: + order_id: '' + preintake_id: '' + handles: {} + result: {} + schema: + description: 反应站反应器取出操作 - 通过订单ID和预取样ID进行精确控制 + properties: + feedback: {} + goal: + properties: + order_id: + description: 订单ID,用于标识要取出的订单 + type: string + preintake_id: + description: 预取样ID,用于标识具体的取样任务 + type: string + required: [] + type: object + result: + properties: + code: + description: 操作结果代码(1表示成功,0表示失败) + type: integer + return_info: + description: 操作结果详细信息 + type: string + type: object + required: + - goal + title: reaction_station_reactor_taken_out参数 + type: object + type: UniLabJsonCommand + reaction_station_solid_feed_vial: + feedback: {} + goal: + assign_material_name: assign_material_name + material_id: material_id + time: time + torque_variation: torque_variation + goal_default: + assign_material_name: '' + material_id: '' + time: '' + torque_variation: '' + handles: {} + result: {} + schema: + description: 反应站固体进料操作 + properties: + feedback: {} + goal: + properties: + assign_material_name: + description: 固体名称_粉末加样模块-投料 + type: string + material_id: + description: 固体投料类型_粉末加样模块-投料 + type: string + time: + description: 观察时间_反应模块-观察搅拌结果 + type: string + torque_variation: + description: 是否观察1否2是_反应模块-观察搅拌结果 + type: string + required: + - assign_material_name + - material_id + - time + - torque_variation + type: object + result: {} + required: + - goal + title: reaction_station_solid_feed_vial参数 + type: object + type: UniLabJsonCommand + reaction_station_take_in: + feedback: {} + goal: + assign_material_name: assign_material_name + cutoff: cutoff + temperature: temperature + goal_default: + assign_material_name: '' + cutoff: '' + temperature: '' + handles: {} + result: {} + schema: + description: 反应站取入操作 + properties: + feedback: {} + goal: + properties: + assign_material_name: + description: 物料名称 + type: string + cutoff: + description: 截止参数 + type: string + temperature: + description: 温度 + type: string + required: + - cutoff + - temperature + - assign_material_name + type: object + result: {} + required: + - goal + title: reaction_station_take_in参数 + type: object + type: UniLabJsonCommand + module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation + protocol_type: [] + status_types: + bioyond_status: dict + enable_dispensing_station: bool + enable_reaction_station: bool + station_type: str + type: python + config_info: [] + description: Bioyond反应站 - 专门用于化学反应操作的工作站 + handles: [] + icon: 反应站.webp + init_param_schema: + config: + properties: + bioyond_config: + description: Bioyond API配置 + properties: + api_host: + description: Bioyond API主机地址 + type: string + api_key: + description: Bioyond API密钥 + type: string + material_type_mappings: + description: 物料类型映射配置 + type: object + workflow_mappings: + description: 工作流映射配置 + type: object + type: object + deck: + description: Deck配置 + type: object + station_config: + description: 反应站配置 + properties: + description: + description: 反应站描述 + type: string + enable_dispensing_station: + default: false + description: 禁用配液站功能 + type: boolean + enable_reaction_station: + default: true + description: 启用反应站功能 + type: boolean + station_name: + description: 反应站名称 + type: string + station_type: + default: reaction_station + description: 站点类型 - 反应站 + enum: + - reaction_station + type: string + type: object + required: [] + type: object + data: + properties: {} + required: [] + type: object + version: 1.0.0 diff --git a/unilabos_msgs/CMakeLists.txt b/unilabos_msgs/CMakeLists.txt index 0725514f..46028fd9 100644 --- a/unilabos_msgs/CMakeLists.txt +++ b/unilabos_msgs/CMakeLists.txt @@ -103,10 +103,14 @@ set(action_files "action/PostProcessGrab.action" "action/PostProcessTriggerClean.action" "action/PostProcessTriggerPostPro.action" - + "action/ReactionStationDripBack.action" - "action/ReactionStationLiquidFeed.action" + "action/ReactionStationLiquidFeedBeaker.action" + "action/ReactionStationLiquidFeedSolvents.action" + "action/ReactionStationLiquidFeedTitration.action" + "action/ReactionStationLiquidFeedVialsNonTitration.action" "action/ReactionStationProExecu.action" + "action/ReactionStationReactorTakenOut.action" "action/ReactionStationReaTackIn.action" "action/ReactionStationSolidFeedVial.action" ) diff --git a/unilabos_msgs/action/ReactionStationDripBack.action b/unilabos_msgs/action/ReactionStationDripBack.action index df690b3b..4d35e963 100644 --- a/unilabos_msgs/action/ReactionStationDripBack.action +++ b/unilabos_msgs/action/ReactionStationDripBack.action @@ -1,11 +1,13 @@ -# Goal - 滴回去 -string volume # 投料体积 -string assign_material_name # 溶剂名称 -string time # 观察时间(单位min) -string torque_variation #是否观察1否2是 +# Goal - 滴回去操作参数 +string volume # 投料体积 +string assign_material_name # 溶剂名称 +string time # 观察时间(单位min) +string torque_variation # 是否观察1否2是 --- -# Result - 操作结果 -string return_info # 结果消息 - +# Result - 操作结果 +bool success # 操作是否成功 +string return_info # 结果消息 +int32 code # 操作结果代码(1表示成功,0表示失败) --- -# Feedback - 实时反馈 +# Feedback - 实时反馈 +string feedback # 操作过程中的反馈信息 diff --git a/unilabos_msgs/action/ReactionStationLiquidFeed.action b/unilabos_msgs/action/ReactionStationLiquidFeed.action deleted file mode 100644 index 8be9dbba..00000000 --- a/unilabos_msgs/action/ReactionStationLiquidFeed.action +++ /dev/null @@ -1,11 +0,0 @@ -# Goal - 液体投料 -string titration_type # 滴定类型1否2是 -string volume # 投料体积 -string assign_material_name # 溶剂名称 -string time # 观察时间(单位min) -string torque_variation #是否观察1否2是 ---- -# Result - 操作结果 -string return_info # 结果消息 ---- -# Feedback - 实时反馈 diff --git a/unilabos_msgs/action/ReactionStationLiquidFeedBeaker.action b/unilabos_msgs/action/ReactionStationLiquidFeedBeaker.action new file mode 100644 index 00000000..ac8d8759 --- /dev/null +++ b/unilabos_msgs/action/ReactionStationLiquidFeedBeaker.action @@ -0,0 +1,15 @@ +# Goal - 液体投料-烧杯操作参数 +string volume # 投料体积 +string assign_material_name # 溶剂名称 +string titration_type # 滴定类型1否2是 +string time # 观察时间(单位min) +string torque_variation # 是否观察1否2是 +string temperature # 温度设置 +--- +# Result - 操作结果 +bool success # 操作是否成功 +string return_info # 结果消息 +int32 code # 操作结果代码(1表示成功,0表示失败) +--- +# Feedback - 实时反馈 +string feedback # 操作过程中的反馈信息 \ No newline at end of file diff --git a/unilabos_msgs/action/ReactionStationLiquidFeedSolvents.action b/unilabos_msgs/action/ReactionStationLiquidFeedSolvents.action new file mode 100644 index 00000000..c89050b7 --- /dev/null +++ b/unilabos_msgs/action/ReactionStationLiquidFeedSolvents.action @@ -0,0 +1,15 @@ +# Goal - 液体投料-溶剂操作参数 +string volume # 投料体积 +string assign_material_name # 溶剂名称 +string titration_type # 滴定类型1否2是 +string time # 观察时间(单位min) +string torque_variation # 是否观察1否2是 +string temperature # 温度设置 +--- +# Result - 操作结果 +bool success # 操作是否成功 +string return_info # 结果消息 +int32 code # 操作结果代码(1表示成功,0表示失败) +--- +# Feedback - 实时反馈 +string feedback # 操作过程中的反馈信息 \ No newline at end of file diff --git a/unilabos_msgs/action/ReactionStationLiquidFeedTitration.action b/unilabos_msgs/action/ReactionStationLiquidFeedTitration.action new file mode 100644 index 00000000..f2885ac9 --- /dev/null +++ b/unilabos_msgs/action/ReactionStationLiquidFeedTitration.action @@ -0,0 +1,15 @@ +# Goal - 液体投料滴定操作参数 +string volume_formula # 投料体积公式 +string assign_material_name # 溶剂名称 +string titration_type # 滴定类型1否2是 +string time # 观察时间(单位min) +string torque_variation # 是否观察1否2是 +string temperature # 温度设置 +--- +# Result - 操作结果 +bool success # 操作是否成功 +string return_info # 结果消息 +int32 code # 操作结果代码(1表示成功,0表示失败) +--- +# Feedback - 实时反馈 +string feedback # 操作过程中的反馈信息 \ No newline at end of file diff --git a/unilabos_msgs/action/ReactionStationLiquidFeedVialsNonTitration.action b/unilabos_msgs/action/ReactionStationLiquidFeedVialsNonTitration.action new file mode 100644 index 00000000..b5e879b2 --- /dev/null +++ b/unilabos_msgs/action/ReactionStationLiquidFeedVialsNonTitration.action @@ -0,0 +1,15 @@ +# Goal - 液体投料-小瓶非滴定操作参数 +string volume_formula # 投料体积公式 +string assign_material_name # 溶剂名称 +string titration_type # 滴定类型1否2是 +string time # 观察时间(单位min) +string torque_variation # 是否观察1否2是 +string temperature # 温度设置 +--- +# Result - 操作结果 +bool success # 操作是否成功 +string return_info # 结果消息 +int32 code # 操作结果代码(1表示成功,0表示失败) +--- +# Feedback - 实时反馈 +string feedback # 操作过程中的反馈信息 \ No newline at end of file diff --git a/unilabos_msgs/action/ReactionStationProExecu.action b/unilabos_msgs/action/ReactionStationProExecu.action index 0c4649a8..ceefc3d4 100644 --- a/unilabos_msgs/action/ReactionStationProExecu.action +++ b/unilabos_msgs/action/ReactionStationProExecu.action @@ -1,8 +1,11 @@ -# Goal - 合并工作流+执行 -string workflow_name # 工作流名称 -string task_name # 任务名称 +# Goal - 合并工作流+执行参数 +string workflow_name # 工作流名称 +string task_name # 任务名称 --- -# Result - 操作结果 -string return_info # 结果消息 +# Result - 操作结果 +bool success # 操作是否成功 +string return_info # 结果消息 +int32 code # 操作结果代码(1表示成功,0表示失败) --- -# Feedback - 实时反馈 +# Feedback - 实时反馈 +string feedback # 操作过程中的反馈信息 diff --git a/unilabos_msgs/action/ReactionStationReaTackIn.action b/unilabos_msgs/action/ReactionStationReaTackIn.action index 78d873ac..51362571 100644 --- a/unilabos_msgs/action/ReactionStationReaTackIn.action +++ b/unilabos_msgs/action/ReactionStationReaTackIn.action @@ -1,9 +1,12 @@ -# Goal - 通量-配置 -string cutoff # 黏度_通量-配置 -string temperature # 温度_通量-配置 -string assign_material_name # 分液类型_通量-配置 +# Goal - 反应器放入操作参数 +string cutoff # 黏度设置 +string temperature # 温度设置 +string assign_material_name # 分液类型 --- -# Result - 操作结果 -string return_info # 结果消息 +# Result - 操作结果 +bool success # 操作是否成功 +string return_info # 结果消息 +int32 code # 操作结果代码(1表示成功,0表示失败) --- -# Feedback - 实时反馈 +# Feedback - 实时反馈 +string feedback # 操作过程中的反馈信息 diff --git a/unilabos_msgs/action/ReactionStationReactorTakenOut.action b/unilabos_msgs/action/ReactionStationReactorTakenOut.action new file mode 100644 index 00000000..29b841b5 --- /dev/null +++ b/unilabos_msgs/action/ReactionStationReactorTakenOut.action @@ -0,0 +1,12 @@ +# Goal - 反应器取出操作参数 +# 反应器取出操作不需要任何参数 +--- +# Result - 操作结果 +# 反应器取出操作的结果 +bool success # 要求必须包含success,以便回传执行结果 +string return_info # 要求必须包含return_info,以便回传执行结果 +int32 code # 操作结果代码(1表示成功,0表示失败) +--- +# Feedback - 实时反馈 +# 反应器取出操作的反馈 +string feedback # 操作过程中的反馈信息 diff --git a/unilabos_msgs/action/ReactionStationSolidFeedVial.action b/unilabos_msgs/action/ReactionStationSolidFeedVial.action index b51096d1..e7e677d5 100644 --- a/unilabos_msgs/action/ReactionStationSolidFeedVial.action +++ b/unilabos_msgs/action/ReactionStationSolidFeedVial.action @@ -1,10 +1,13 @@ -# Goal - 固体投料-小瓶 -string assign_material_name # 固体名称_粉末加样模块-投料 -string material_id # 固体投料类型_粉末加样模块-投料 -string time # 观察时间_反应模块-观察搅拌结果 -string torque_variation #是否观察1否2是_反应模块-观察搅拌结果 +# Goal - 固体投料-小瓶操作参数 +string assign_material_name # 固体名称 +string material_id # 固体投料类型 +string time # 观察时间(单位min) +string torque_variation # 是否观察1否2是 --- -# Result - 操作结果 -string return_info # 结果消息 +# Result - 操作结果 +bool success # 操作是否成功 +string return_info # 结果消息 +int32 code # 操作结果代码(1表示成功,0表示失败) --- -# Feedback - 实时反馈 +# Feedback - 实时反馈 +string feedback # 操作过程中的反馈信息