diff --git a/test/experiments/reaction_station_bioyond.json b/test/experiments/reaction_station_bioyond.json index 013855ed..ca8bd683 100644 --- a/test/experiments/reaction_station_bioyond.json +++ b/test/experiments/reaction_station_bioyond.json @@ -24,13 +24,13 @@ "Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a" }, "material_type_mappings": { - "烧杯": ["BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"], - "试剂瓶": ["BIOYOND_PolymerStation_1BottleCarrier", ""], - "样品板": ["BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"], - "分装板": ["BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"], - "样品瓶": ["BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"], - "90%分装小瓶": ["BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"], - "10%分装小瓶": ["BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"] + "烧杯": ["YB_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"], + "试剂瓶": ["YB_1BottleCarrier", ""], + "样品板": ["YB_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"], + "分装板": ["YB_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"], + "样品瓶": ["YB_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"], + "90%分装小瓶": ["YB_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"], + "10%分装小瓶": ["YB_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"] } }, "deck": { diff --git a/test/experiments/reaction_station_bioyond_test.json b/test/experiments/reaction_station_bioyond_test.json index 8446373a..67a0d468 100644 --- a/test/experiments/reaction_station_bioyond_test.json +++ b/test/experiments/reaction_station_bioyond_test.json @@ -24,9 +24,9 @@ "Drip_back": "3a162cf9-6aac-565a-ddd7-682ba1796a4a" }, "material_type_mappings": { - "烧杯": "BIOYOND_PolymerStation_1FlaskCarrier", - "试剂瓶": "BIOYOND_PolymerStation_1BottleCarrier", - "样品板": "BIOYOND_PolymerStation_6VialCarrier" + "烧杯": "YB_1FlaskCarrier", + "试剂瓶": "YB_1BottleCarrier", + "样品板": "YB_6VialCarrier" } }, "deck": { diff --git a/test/resources/test_bottle_carrier.py b/test/resources/test_bottle_carrier.py index c981eeeb..f0a2749f 100644 --- a/test/resources/test_bottle_carrier.py +++ b/test/resources/test_bottle_carrier.py @@ -1,7 +1,7 @@ import pytest 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 +from unilabos.resources.bioyond.bottles import YB_Solid_Vial, YB_Solution_Beaker, YB_Reagent_Bottle def test_bottle_carrier() -> "BottleCarrier": @@ -16,9 +16,9 @@ def test_bottle_carrier() -> "BottleCarrier": print(f"1烧杯载架: {beaker_carrier.name}, 位置数: {len(beaker_carrier.sites)}") # 创建瓶子和烧杯 - 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") + powder_bottle = YB_Solid_Vial("powder_bottle_01") + solution_beaker = YB_Solution_Beaker("solution_beaker_01") + reagent_bottle = YB_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 index 068a0530..d87415b9 100644 --- a/test/resources/test_converter_bioyond.py +++ b/test/resources/test_converter_bioyond.py @@ -12,13 +12,13 @@ lab_registry.setup() type_mapping = { - "烧杯": ("BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"), - "试剂瓶": ("BIOYOND_PolymerStation_1BottleCarrier", ""), - "样品板": ("BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"), - "分装板": ("BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"), - "样品瓶": ("BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"), - "90%分装小瓶": ("BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"), - "10%分装小瓶": ("BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"), + "烧杯": ("YB_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"), + "试剂瓶": ("YB_1BottleCarrier", ""), + "样品板": ("YB_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"), + "分装板": ("YB_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"), + "样品瓶": ("YB_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"), + "90%分装小瓶": ("YB_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"), + "10%分装小瓶": ("YB_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"), } diff --git a/test/resources/test_resourcetreeset.py b/test/resources/test_resourcetreeset.py index 1ba9ab20..ff5cfd06 100644 --- a/test/resources/test_resourcetreeset.py +++ b/test/resources/test_resourcetreeset.py @@ -13,13 +13,13 @@ lab_registry.setup() type_mapping = { - "烧杯": ("BIOYOND_PolymerStation_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"), - "试剂瓶": ("BIOYOND_PolymerStation_1BottleCarrier", ""), - "样品板": ("BIOYOND_PolymerStation_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"), - "分装板": ("BIOYOND_PolymerStation_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"), - "样品瓶": ("BIOYOND_PolymerStation_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"), - "90%分装小瓶": ("BIOYOND_PolymerStation_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"), - "10%分装小瓶": ("BIOYOND_PolymerStation_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"), + "烧杯": ("YB_1FlaskCarrier", "3a14196b-24f2-ca49-9081-0cab8021bf1a"), + "试剂瓶": ("YB_1BottleCarrier", ""), + "样品板": ("YB_6StockCarrier", "3a14196e-b7a0-a5da-1931-35f3000281e9"), + "分装板": ("YB_6VialCarrier", "3a14196e-5dfe-6e21-0c79-fe2036d052c4"), + "样品瓶": ("YB_Solid_Stock", "3a14196a-cf7d-8aea-48d8-b9662c7dba94"), + "90%分装小瓶": ("YB_Solid_Vial", "3a14196c-cdcf-088d-dc7d-5cf38f0ad9ea"), + "10%分装小瓶": ("YB_Liquid_Vial", "3a14196c-76be-2279-4e22-7310d69aed68"), } diff --git a/unilabos/devices/workstation/bioyond_studio/config.py b/unilabos/devices/workstation/bioyond_studio/config.py index 81a97362..4804da13 100644 --- a/unilabos/devices/workstation/bioyond_studio/config.py +++ b/unilabos/devices/workstation/bioyond_studio/config.py @@ -26,7 +26,7 @@ WAREHOUSE_MAPPING = { "粉末加样头堆栈": { "uuid": "", "site_uuids": { - "A01": "3a19da56-1379-20c8-5886-f7c4fbcb5733", + "A01": "3a19da56-1379-ff7c-1745-07e200b44ce2", "B01": "3a19da56-1379-2424-d751-fe6e94cef938", "C01": "3a19da56-1379-271c-03e3-6bdb590e395e", "D01": "3a19da56-1379-277f-2b1b-0d11f7cf92c6", @@ -47,7 +47,89 @@ WAREHOUSE_MAPPING = { "S01": "3a19da56-1379-f924-7f68-df1fa51489f4", "T01": "3a19da56-1379-ff7c-1745-07e200b44ce2" } - } + }, + "配液站内试剂仓库": { + "uuid": "", + "site_uuids": { + "A01": "3a19da43-57b5-294f-d663-154a1cc32270", + "B01": "3a19da43-57b5-7394-5f49-54efe2c9bef2", + "C01": "3a19da43-57b5-5e75-552f-8dbd0ad1075f", + "A02": "3a19da43-57b5-8441-db94-b4d3875a4b6c", + "B02": "3a19da43-57b5-3e41-c181-5119dddaf50c", + "C02": "3a19da43-57b5-269b-282d-fba61fe8ce96", + "A03": "3a19da43-57b5-7c1e-d02e-c40e8c33f8a1", + "B03": "3a19da43-57b5-659f-621f-1dcf3f640363", + "C03": "3a19da43-57b5-855a-6e71-f398e376dee1", + } + }, + "试剂替换仓库": { + "uuid": "", + "site_uuids": { + "A01": "3a19da51-8f4e-30f3-ea08-4f8498e9b097", + "B01": "3a19da51-8f4e-1da7-beb0-80a4a01e67a8", + "C01": "3a19da51-8f4e-337d-2675-bfac46880b06", + "D01": "3a19da51-8f4e-e514-b92c-9c44dc5e489d", + "E01": "3a19da51-8f4e-22d1-dd5b-9774ddc80402", + "F01": "3a19da51-8f4e-273a-4871-dff41c29bfd9", + "G01": "3a19da51-8f4e-b32f-454f-74bc1a665653", + "H01": "3a19da51-8f4e-8c93-68c9-0b4382320f59", + "I01": "3a19da51-8f4e-360c-0149-291b47c6089b", + "J01": "3a19da51-8f4e-4152-9bca-8d64df8c1af0" + } + }, + "自动堆栈-左": { + "uuid": "", + "site_uuids": { + "A01": "3a19debc-84b5-4c1c-d3a1-26830cf273ff", + "A02": "3a19debc-84b5-033b-b31f-6b87f7c2bf52", + "B01": "3a19debc-84b5-3924-172f-719ab01b125c", + "B02": "3a19debc-84b5-aad8-70c6-b8c6bb2d8750" + } + }, + "自动堆栈-右": { + "uuid": "", + "site_uuids": { + "A01": "3a19debe-5200-7df2-1dd9-7d202f158864", + "A02": "3a19debe-5200-573b-6120-8b51f50e1e50", + "B01": "3a19debe-5200-7cd8-7666-851b0a97e309", + "B02": "3a19debe-5200-e6d3-96a3-baa6e3d5e484" + } + }, + "手动堆栈": { + "uuid": "", + "site_uuids": { + "A01": "3a19deae-2c7a-36f5-5e41-02c5b66feaea", + "A02": "3a19deae-2c7a-dc6d-c41e-ef285d946cfe", + "A03": "3a19deae-2c7a-5876-c454-6b7e224ca927", + "B01": "3a19deae-2c7a-2426-6d71-e9de3cb158b1", + "B02": "3a19deae-2c7a-79b0-5e44-efaafd1e4cf3", + "B03": "3a19deae-2c7a-b9eb-f4e3-e308e0cf839a", + "C01": "3a19deae-2c7a-32bc-768e-556647e292f3", + "C02": "3a19deae-2c7a-e97a-8484-f5a4599447c4", + "C03": "3a19deae-2c7a-3056-6504-10dc73fbc276", + "D01": "3a19deae-2c7a-ffad-875e-8c4cda61d440", + "D02": "3a19deae-2c7a-61be-601c-b6fb5610499a", + "D03": "3a19deae-2c7a-c0f7-05a7-e3fe2491e560", + "E01": "3a19deae-2c7a-a6f4-edd1-b436a7576363", + "E02": "3a19deae-2c7a-4367-96dd-1ca2186f4910", + "E03": "3a19deae-2c7a-b163-2219-23df15200311", + "F01": "3a19deae-2c7a-d594-fd6a-0d20de3c7c4a", + "F02": "3a19deae-2c7a-a194-ea63-8b342b8d8679", + "F03": "3a19deae-2c7a-f7c4-12bd-425799425698", + "G01": "3a19deae-2c7a-0b56-72f1-8ab86e53b955", + "G02": "3a19deae-2c7a-204e-95ed-1f1950f28343", + "G03": "3a19deae-2c7a-392b-62f1-4907c66343f8", + "H01": "3a19deae-2c7a-5602-e876-d27aca4e3201", + "H02": "3a19deae-2c7a-f15c-70e0-25b58a8c9702", + "H03": "3a19deae-2c7a-780b-8965-2e1345f7e834", + "I01": "3a19deae-2c7a-8849-e172-07de14ede928", + "I02": "3a19deae-2c7a-4772-a37f-ff99270bafc0", + "I03": "3a19deae-2c7a-cce7-6e4a-25ea4a2068c4", + "J01": "3a19deae-2c7a-1848-de92-b5d5ed054cc6", + "J02": "3a19deae-2c7a-1d45-b4f8-6f866530e205", + "J03": "3a19deae-2c7a-f237-89d9-8fe19025dee9" + } + }, } # 物料类型配置 @@ -88,10 +170,10 @@ SOLID_LIQUID_MAPPINGS = { # "barCode": "", # "name": "LiFSI", # "unit": "g", - # "parameters": {"Density": "1.533"}, + # "parameters": "", # "quantity": 2, # "warningQuantity": 1, - # "details": [{}] + # "details": [] # }, # "DTC": { # "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469", @@ -99,10 +181,10 @@ SOLID_LIQUID_MAPPINGS = { # "barCode": "", # "name": "DTC", # "unit": "g", - # "parameters": {"Density": "1.533"}, + # "parameters": "", # "quantity": 2, # "warningQuantity": 1, - # "details": [{}] + # "details": [] # }, # "LiPO2F2": { # "typeId": "3a190ca0-b2f6-9aeb-8067-547e72c11469", @@ -110,10 +192,10 @@ SOLID_LIQUID_MAPPINGS = { # "barCode": "", # "name": "LiPO2F2", # "unit": "g", - # "parameters": {"Density": "1.533"}, + # "parameters": "", # "quantity": 2, # "warningQuantity": 1, - # "details": [{}] + # "details": [] # }, # 液体 # "SA": ("BIOYOND_PolymerStation_Solid_Stock", "3a190ca0-b2f6-9aeb-8067-547e72c11469"), diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/__init__.py b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/button_battery_station.py b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/button_battery_station.py new file mode 100644 index 00000000..eae09b84 --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/button_battery_station.py @@ -0,0 +1,1006 @@ +""" +纽扣电池组装工作站物料类定义 +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 # 材料类型(正极、负极、隔膜、弹片、垫片、铝箔等) + height: float + electrolyte_name: str + data_electrolyte_code: str + open_circuit_voltage: float + assembly_pressure: float + electrolyte_volume: float + + 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): + """子弹夹洞位类""" + + def __init__( + self, + name: str, + diameter: float, + depth: float, + max_sheets: int = 100, + category: str = "clip_magazine_hole", + ): + """初始化子弹夹洞位 + + Args: + name: 洞位名称 + diameter: 洞直径 (mm) + depth: 洞深度 (mm) + max_sheets: 最大极片数量 + category: 类别 + """ + super().__init__( + name=name, + size_x=diameter, + size_y=diameter, + size_z=depth, + category=category, + ) + self.diameter = diameter + self.depth = depth + self.max_sheets = max_sheets + self._sheets: List[ElectrodeSheet] = [] + + def can_add_sheet(self, sheet: ElectrodeSheet) -> bool: + """检查是否可以添加极片""" + return (len(self._sheets) < self.max_sheets and + sheet.diameter <= self.diameter) + + def add_sheet(self, sheet: ElectrodeSheet) -> None: + """添加极片""" + if not self.can_add_sheet(sheet): + raise ValueError(f"无法向洞位 {self.name} 添加极片") + self._sheets.append(sheet) + + def take_sheet(self) -> ElectrodeSheet: + """取出极片""" + if len(self._sheets) == 0: + raise ValueError(f"洞位 {self.name} 没有极片") + return self._sheets.pop() + + def get_sheet_count(self) -> int: + """获取极片数量""" + return len(self._sheets) + + def serialize_state(self) -> Dict[str, Any]: + return { + "sheet_count": len(self._sheets), + "sheets": [sheet.serialize() for sheet in self._sheets], + } + +# TODO: 这个要改 +class ClipMagazine(Resource): + """子弹夹类 - 有6个洞位,每个洞位放多个极片""" + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + 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=0, + depth=0, + ) + + 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 + assembly_pressure: float + electrolyte_volume: float + electrolyte_name: str + +class Battery(Resource): + """电池类 - 可容纳极片""" + children: List[ElectrodeSheet] = [] + + def __init__( + self, + name: str, + size_x=1, + size_y=1, + size_z=1, + 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( + diameter = 1.0, + height = 1.0, + assembly_pressure = 1.0, + electrolyte_volume = 1.0, + electrolyte_name = "DP001" + ) + + 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() + 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, + ) + + + +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 BottleRack(Resource): + """瓶架类 - 12个待配位置+12个已配位置""" + children: List[Bottle] = [] + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + category: str = "bottle_rack", + model: Optional[str] = None, + ): + """初始化瓶架 + + 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, + ) + # TODO: 添加瓶位坐标映射 + self.index_to_pos = { + 0: Coordinate.zero(), + 1: Coordinate(x=1, y=2, z=3) # 添加 + } + self.name_to_index = {} + self.name_to_pos = {} + + 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(self, resource: Bottle, location=Coordinate.zero(), reassign = True): + assert len(self.children) <= 12, "瓶架已满,无法添加更多瓶子" + index = len(self.children) + location = Coordinate(x=20 + (index % 4) * 15, y=20 + (index // 4) * 15, z=0) + 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_by_index(self, resource: Bottle, index: int): + assert 0 <= index < 12, "无效的瓶子索引" + 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): + # self.children.sort(key=lambda x: self.name_to_index.get(x.name, 0)) + # return super().serialize() + + +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 + +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(size_x=1000, + size_y=1000, + 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"jipian1_{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="电池料盘", size_x=120.8, size_y=160.5, size_z=10.0, fill=True) + #把物料板放到桌子上 + deck.assign_child_resource(liaopan3, Coordinate(x=100, y=100, z=0)) + + + + #liaopan.children[3].assign_child_resource(jipian, location=None) + print(deck) + + + from unilabos.resources.graphio import convert_resources_from_type + from unilabos.config.config import BasicConfig + BasicConfig.ak = "4d5ce6ae-7234-4639-834e-93899b9caf94" + BasicConfig.sk = "505d3b0a-620e-459a-9905-1efcffce382a" + 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/coin_cell_assembly.py b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/coin_cell_assembly.py new file mode 100644 index 00000000..750a34fb --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/coin_cell_assembly.py @@ -0,0 +1,1172 @@ +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.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, + station_resource: CoincellDeck, + address: str = "192.168.1.20", + port: str = "502", + debug_mode: bool = True, + *args, + **kwargs, + ): + super().__init__( + #桌子 + 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.coin_num_N = 0 #已组装电池数量 + #创建一个物料台面,包含两个极片板 + #self.deck = create_a_coin_cell_deck() + + #self._ros_node.update_resource(self.deck) + + #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): + bottle_num = int(bottle_num) + #发送电解液平台数 + 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, elec_vol, assembly_type, assembly_pressure) -> 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) + #设置平行样数目 + 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) + 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 + liaopan3 = self.station_resource.get_resource("\u7535\u6c60\u6599\u76d8") + #把物料解绑后放到另一盘上 + battery = ElectrodeSheet(name=f"battery_{self.coin_num_N}", size_x=14, size_y=14, size_z=2) + battery._unilabos_state = { + "electrolyte_name": data_coin_cell_code, + "data_electrolyte_code": data_electrolyte_code, + "open_circuit_voltage": data_open_circuit_voltage, + "assembly_pressure": data_assembly_pressure, + "electrolyte_volume": data_electrolyte_volume + } + liaopan3.children[self.coin_num_N].assign_child_resource(battery, location=None) + #print(jipian2.parent) + ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ + "resources": [self.station_resource] + }) + + + 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, elec_vol:int=50, assembly_type:int=7, assembly_pressure:int=4200, file_path: str="D:\\coin_cell_data") -> bool: + elec_num, elec_use_num, elec_vol, assembly_type, assembly_pressure = int(elec_num), int(elec_use_num), int(elec_vol), int(assembly_type), int(assembly_pressure) + 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 + for i in range(20): + print(f"剩余电解液瓶数: {elec_num}, 已组装电池数: {elec_use_num}") + print(f"剩余电解液瓶数: {type(elec_num)}, 已组装电池数: {type(elec_use_num)}") + print(f"剩余电解液瓶数: {type(int(elec_num))}, 已组装电池数: {type(int(elec_use_num))}") + + #如果是第一次运行,则进行初始化、切换自动、启动, 如果是断点重启则跳过。 + if read_status_flag == False: + pass + #初始化 + #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, elec_vol, assembly_type, assembly_pressure) + + 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:读完再将电池数加一还是进入循环就将电池数加一需要考虑 + + + + # 生成断点文件 + # 生成包含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 + self.coin_num_N = coin_num_N + 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个物料盘 + liaopan3 = self.station_resource.get_resource("\u7535\u6c60\u6599\u76d8") + for i in range(16): + battery = ElectrodeSheet(name=f"battery_{i}", size_x=16, size_y=16, size_z=2) + battery._unilabos_state = { + "diameter": 20.0, + "height": 20.0, + "assembly_pressure": i, + "electrolyte_volume": 20.0, + "electrolyte_name": f"DP{i}" + } + liaopan3.children[i].assign_child_resource(battery, location=None) + + ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ + "resources": [self.station_resource] + }) + time.sleep(4) + # 数据读取与输出 + 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() + #deck = create_a_full_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) + + liaopan1 = deck.get_resource("liaopan1") + liaopan2 = deck.get_resource("liaopan2") + for i in range(16): + #找到liaopan1上每一个jipian + jipian_linshi = liaopan1.children[i].children[0] + #把物料解绑后放到另一盘上 + print("极片:", jipian_linshi) + jipian_linshi.parent.unassign_child_resource(jipian_linshi) + liaopan2.children[i].assign_child_resource(jipian_linshi, location=None) + + + 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) + + + + from unilabos.resources.graphio import convert_resources_from_type + from unilabos.config.config import BasicConfig + BasicConfig.ak = "beb0c15f-2279-46a1-aba5-00eaf89aef55" + BasicConfig.sk = "15d4f25e-3512-4f9c-9bfb-43ab85e7b561" + 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/coin_cell_assembly_a.csv b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/coin_cell_assembly_a.csv new file mode 100644 index 00000000..836fb712 --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/coin_cell_assembly_a.csv @@ -0,0 +1,63 @@ +Name,DataType,InitValue,Comment,Attribute,DeviceType,Address, +COIL_SYS_START_CMD,BOOL,,,,coil,8010, +COIL_SYS_STOP_CMD,BOOL,,,,coil,8020, +COIL_SYS_RESET_CMD,BOOL,,,,coil,8030, +COIL_SYS_HAND_CMD,BOOL,,,,coil,8040, +COIL_SYS_AUTO_CMD,BOOL,,,,coil,8050, +COIL_SYS_INIT_CMD,BOOL,,,,coil,8060, +COIL_UNILAB_SEND_MSG_SUCC_CMD,BOOL,,,,coil,8700, +COIL_UNILAB_REC_MSG_SUCC_CMD,BOOL,,,,coil,8710,unilab_rec_msg_succ_cmd +COIL_SYS_START_STATUS,BOOL,,,,coil,8210, +COIL_SYS_STOP_STATUS,BOOL,,,,coil,8220, +COIL_SYS_RESET_STATUS,BOOL,,,,coil,8230, +COIL_SYS_HAND_STATUS,BOOL,,,,coil,8240, +COIL_SYS_AUTO_STATUS,BOOL,,,,coil,8250, +COIL_SYS_INIT_STATUS,BOOL,,,,coil,8260, +COIL_REQUEST_REC_MSG_STATUS,BOOL,,,,coil,8500, +COIL_REQUEST_SEND_MSG_STATUS,BOOL,,,,coil,8510,request_send_msg_status +REG_MSG_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,11000, +REG_MSG_ELECTROLYTE_NUM,INT16,,,,hold_register,11002,unilab_send_msg_electrolyte_num +REG_MSG_ELECTROLYTE_VOLUME,INT16,,,,hold_register,11004,unilab_send_msg_electrolyte_vol +REG_MSG_ASSEMBLY_TYPE,INT16,,,,hold_register,11006,unilab_send_msg_assembly_type +REG_MSG_ASSEMBLY_PRESSURE,INT16,,,,hold_register,11008,unilab_send_msg_assembly_pressure +REG_DATA_ASSEMBLY_COIN_CELL_NUM,INT16,,,,hold_register,10000,data_assembly_coin_cell_num +REG_DATA_OPEN_CIRCUIT_VOLTAGE,FLOAT32,,,,hold_register,10002,data_open_circuit_voltage +REG_DATA_AXIS_X_POS,FLOAT32,,,,hold_register,10004, +REG_DATA_AXIS_Y_POS,FLOAT32,,,,hold_register,10006, +REG_DATA_AXIS_Z_POS,FLOAT32,,,,hold_register,10008, +REG_DATA_POLE_WEIGHT,FLOAT32,,,,hold_register,10010,data_pole_weight +REG_DATA_ASSEMBLY_PER_TIME,FLOAT32,,,,hold_register,10012,data_assembly_time +REG_DATA_ASSEMBLY_PRESSURE,INT16,,,,hold_register,10014,data_assembly_pressure +REG_DATA_ELECTROLYTE_VOLUME,INT16,,,,hold_register,10016,data_electrolyte_volume +REG_DATA_COIN_NUM,INT16,,,,hold_register,10018,data_coin_num +REG_DATA_ELECTROLYTE_CODE,STRING,,,,hold_register,10020,data_electrolyte_code() +REG_DATA_COIN_CELL_CODE,STRING,,,,hold_register,10030,data_coin_cell_code() +REG_DATA_STACK_VISON_CODE,STRING,,,,hold_register,12004,data_stack_vision_code() +REG_DATA_GLOVE_BOX_PRESSURE,FLOAT32,,,,hold_register,10050,data_glove_box_pressure +REG_DATA_GLOVE_BOX_WATER_CONTENT,FLOAT32,,,,hold_register,10052,data_glove_box_water_content +REG_DATA_GLOVE_BOX_O2_CONTENT,FLOAT32,,,,hold_register,10054,data_glove_box_o2_content +UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8720, +UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM,BOOL,,,,coil,8520, +REG_MSG_ELECTROLYTE_NUM_USED,INT16,,,,hold_register,496, +REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,10000, +UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,8730, +UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,8530, +REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,10018,ASSEMBLY_TYPE7or8 +COIL_ALUMINUM_FOIL,BOOL,,ʹ,,coil,8340, +REG_MSG_NE_PLATE_MATRIX,INT16,,Ƭλ,,hold_register,440, +REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,Ĥλ,,hold_register,450, +REG_MSG_TIP_BOX_MATRIX,INT16,,Һǹͷλ,,hold_register,480, +REG_MSG_NE_PLATE_NUM,INT16,,Ƭ,,hold_register,443, +REG_MSG_SEPARATOR_PLATE_NUM,INT16,,Ĥ,,hold_register,453, +REG_MSG_PRESS_MODE,BOOL,,ѹģʽfalse:ѹģʽTrue:ģʽ,,coil,8360,ѹģʽ +,,,,,,, +,BOOL,,Ӿλfalse:ʹãtrue:ԣ,,coil,8300,Ӿλ +,BOOL,,죨false:ʹãtrue:ԣ,,coil,8310,Ӿ +,BOOL,,_֣false:ʹãtrue:ԣ,,coil,8320, +,BOOL,,_Ҳ֣false:ʹãtrue:ԣ,,coil,8420,Ҳ +,BOOL,,ռ֪false:ʹãtrue:ԣ,,coil,8350,ռ֪ +,BOOL,,Һģʽfalse:εҺtrue:εҺ,,coil,8370,Һģʽ +,BOOL,,Ƭأfalse:ʹãtrue:ԣ,,coil,8380,Ƭ +,BOOL,,Ƭװʽfalse:װtrue:װ,,coil,8390,װ +,BOOL,,ѹࣨfalse:ʹãtrue:ԣ,,coil,8400,ѹ +,BOOL,,̷̰ʽfalse:ˮƽ̣true:ѵ̣,,coil,8410,Ƭ̷ʽ diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/new_cellconfig3c.json b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/new_cellconfig3c.json new file mode 100644 index 00000000..630faa58 --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/new_cellconfig3c.json @@ -0,0 +1,691 @@ +{ + "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": [ + "\u7535\u6c60\u6599\u76d8" + ], + "parent": null, + "type": "container", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "CoincellDeck", + "size_x": 1000, + "size_y": 1000, + "size_z": 900, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "coin_cell_deck", + "barcode": null + }, + "data": {} + }, + { + "id": "\u7535\u6c60\u6599\u76d8", + "name": "\u7535\u6c60\u6599\u76d8", + "sample_id": null, + "children": [ + "\u7535\u6c60\u6599\u76d8_materialhole_0_0", + "\u7535\u6c60\u6599\u76d8_materialhole_0_1", + "\u7535\u6c60\u6599\u76d8_materialhole_0_2", + "\u7535\u6c60\u6599\u76d8_materialhole_0_3", + "\u7535\u6c60\u6599\u76d8_materialhole_1_0", + "\u7535\u6c60\u6599\u76d8_materialhole_1_1", + "\u7535\u6c60\u6599\u76d8_materialhole_1_2", + "\u7535\u6c60\u6599\u76d8_materialhole_1_3", + "\u7535\u6c60\u6599\u76d8_materialhole_2_0", + "\u7535\u6c60\u6599\u76d8_materialhole_2_1", + "\u7535\u6c60\u6599\u76d8_materialhole_2_2", + "\u7535\u6c60\u6599\u76d8_materialhole_2_3", + "\u7535\u6c60\u6599\u76d8_materialhole_3_0", + "\u7535\u6c60\u6599\u76d8_materialhole_3_1", + "\u7535\u6c60\u6599\u76d8_materialhole_3_2", + "\u7535\u6c60\u6599\u76d8_materialhole_3_3" + ], + "parent": "coin_cell_deck", + "type": "container", + "class": "", + "position": { + "x": 100, + "y": 100, + "z": 0 + }, + "config": { + "type": "MaterialPlate", + "size_x": 120.8, + "size_y": 160.5, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_plate", + "model": null, + "barcode": null, + "ordering": { + "A1": "\u7535\u6c60\u6599\u76d8_materialhole_0_0", + "B1": "\u7535\u6c60\u6599\u76d8_materialhole_0_1", + "C1": "\u7535\u6c60\u6599\u76d8_materialhole_0_2", + "D1": "\u7535\u6c60\u6599\u76d8_materialhole_0_3", + "A2": "\u7535\u6c60\u6599\u76d8_materialhole_1_0", + "B2": "\u7535\u6c60\u6599\u76d8_materialhole_1_1", + "C2": "\u7535\u6c60\u6599\u76d8_materialhole_1_2", + "D2": "\u7535\u6c60\u6599\u76d8_materialhole_1_3", + "A3": "\u7535\u6c60\u6599\u76d8_materialhole_2_0", + "B3": "\u7535\u6c60\u6599\u76d8_materialhole_2_1", + "C3": "\u7535\u6c60\u6599\u76d8_materialhole_2_2", + "D3": "\u7535\u6c60\u6599\u76d8_materialhole_2_3", + "A4": "\u7535\u6c60\u6599\u76d8_materialhole_3_0", + "B4": "\u7535\u6c60\u6599\u76d8_materialhole_3_1", + "C4": "\u7535\u6c60\u6599\u76d8_materialhole_3_2", + "D4": "\u7535\u6c60\u6599\u76d8_materialhole_3_3" + } + }, + "data": {} + }, + { + "id": "\u7535\u6c60\u6599\u76d8_materialhole_0_0", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_0_0", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "container", + "class": "", + "position": { + "x": 12.4, + "y": 104.25, + "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": "\u7535\u6c60\u6599\u76d8_materialhole_0_1", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_0_1", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "container", + "class": "", + "position": { + "x": 12.4, + "y": 80.25, + "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": "\u7535\u6c60\u6599\u76d8_materialhole_0_2", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_0_2", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "container", + "class": "", + "position": { + "x": 12.4, + "y": 56.25, + "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": "\u7535\u6c60\u6599\u76d8_materialhole_0_3", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_0_3", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "container", + "class": "", + "position": { + "x": 12.4, + "y": 32.25, + "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": "\u7535\u6c60\u6599\u76d8_materialhole_1_0", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_1_0", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "container", + "class": "", + "position": { + "x": 36.4, + "y": 104.25, + "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": "\u7535\u6c60\u6599\u76d8_materialhole_1_1", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_1_1", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "container", + "class": "", + "position": { + "x": 36.4, + "y": 80.25, + "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": "\u7535\u6c60\u6599\u76d8_materialhole_1_2", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_1_2", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "container", + "class": "", + "position": { + "x": 36.4, + "y": 56.25, + "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": "\u7535\u6c60\u6599\u76d8_materialhole_1_3", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_1_3", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "container", + "class": "", + "position": { + "x": 36.4, + "y": 32.25, + "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": "\u7535\u6c60\u6599\u76d8_materialhole_2_0", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_2_0", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "container", + "class": "", + "position": { + "x": 60.4, + "y": 104.25, + "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": "\u7535\u6c60\u6599\u76d8_materialhole_2_1", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_2_1", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "container", + "class": "", + "position": { + "x": 60.4, + "y": 80.25, + "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": "\u7535\u6c60\u6599\u76d8_materialhole_2_2", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_2_2", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "container", + "class": "", + "position": { + "x": 60.4, + "y": 56.25, + "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": "\u7535\u6c60\u6599\u76d8_materialhole_2_3", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_2_3", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "container", + "class": "", + "position": { + "x": 60.4, + "y": 32.25, + "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": "\u7535\u6c60\u6599\u76d8_materialhole_3_0", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_3_0", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "container", + "class": "", + "position": { + "x": 84.4, + "y": 104.25, + "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": "\u7535\u6c60\u6599\u76d8_materialhole_3_1", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_3_1", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "container", + "class": "", + "position": { + "x": 84.4, + "y": 80.25, + "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": "\u7535\u6c60\u6599\u76d8_materialhole_3_2", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_3_2", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "container", + "class": "", + "position": { + "x": 84.4, + "y": 56.25, + "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": "\u7535\u6c60\u6599\u76d8_materialhole_3_3", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_3_3", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "container", + "class": "", + "position": { + "x": 84.4, + "y": 32.25, + "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 + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/unilabos/devices/workstation/coin_cell_assembly/workstation_base.py b/unilabos/devices/workstation/coin_cell_assembly/workstation_base.py new file mode 100644 index 00000000..65b34a93 --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/workstation_base.py @@ -0,0 +1,489 @@ +""" +工作站基类 +Workstation Base Class - 简化版 + +基于PLR Deck的简化工作站架构 +专注于核心物料系统和工作流管理 +""" + +import collections +import time +from typing import Dict, Any, List, Optional, Union +from abc import ABC, abstractmethod +from dataclasses import dataclass +from enum import Enum +from pylabrobot.resources import Deck, Plate, Resource as PLRResource + +from pylabrobot.resources.coordinate import Coordinate +from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode + +from unilabos.utils.log import logger + + +class WorkflowStatus(Enum): + """工作流状态""" + + IDLE = "idle" + INITIALIZING = "initializing" + RUNNING = "running" + PAUSED = "paused" + STOPPING = "stopping" + STOPPED = "stopped" + ERROR = "error" + COMPLETED = "completed" + + +@dataclass +class WorkflowInfo: + """工作流信息""" + + name: str + description: str + estimated_duration: float # 预估持续时间(秒) + required_materials: List[str] # 所需物料类型 + output_product: str # 输出产品类型 + parameters_schema: Dict[str, Any] # 参数架构 + + +class WorkStationContainer(Plate): + """ + WorkStation 专用 Container 类,继承自 Plate和TipRack + 注意这个物料必须通过plr_additional_res_reg.py注册到edge,才能正常序列化 + """ + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + category: str, + ordering: collections.OrderedDict, + model: Optional[str] = None, + ): + """ + 这里的初始化入参要和plr的保持一致 + """ + super().__init__(name, size_x, size_y, size_z, category=category, ordering=ordering, model=model) + 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 + + +def get_workstation_plate_resource(name: str) -> PLRResource: # 要给定一个返回plr的方法 + """ + 用于获取一些模板,例如返回一个带有特定信息/子物料的 Plate,这里需要到注册表注册,例如unilabos/registry/resources/organic/workstation.yaml + 可以直接运行该函数或者利用注册表补全机制,来检查是否资源出错 + :param name: 资源名称 + :return: Resource对象 + """ + plate = WorkStationContainer( + name, size_x=50, size_y=50, size_z=10, category="plate", ordering=collections.OrderedDict() + ) + tip_rack = WorkStationContainer( + "tip_rack_inside_plate", + size_x=50, + size_y=50, + size_z=10, + category="tip_rack", + ordering=collections.OrderedDict(), + ) + plate.assign_child_resource(tip_rack, Coordinate.zero()) + return plate + + +class ResourceSynchronizer(ABC): + """资源同步器基类 + + 负责与外部物料系统的同步,并对 self.deck 做修改 + """ + + def __init__(self, workstation: "WorkstationBase"): + self.workstation = workstation + + @abstractmethod + async def sync_from_external(self) -> bool: + """从外部系统同步物料到本地deck""" + pass + + @abstractmethod + async def sync_to_external(self, plr_resource: PLRResource) -> bool: + """将本地物料同步到外部系统""" + pass + + @abstractmethod + async def handle_external_change(self, change_info: Dict[str, Any]) -> bool: + """处理外部系统的变更通知""" + pass + + +class WorkstationBase(ABC): + """工作站基类 - 简化版 + + 核心功能: + 1. 基于 PLR Deck 的物料系统,支持格式转换 + 2. 可选的资源同步器支持外部物料系统 + 3. 简化的工作流管理 + """ + + _ros_node: ROS2WorkstationNode + + @property + def _children(self) -> Dict[str, Any]: # 不要删除这个下划线,不然会自动导入注册表,后面改成装饰器识别 + return self._ros_node.children + + async def update_resource_example(self): + return await self._ros_node.update_resource([get_workstation_plate_resource("test")]) + + def __init__( + self, + station_resource: PLRResource, + *args, + **kwargs, # 必须有kwargs + ): + # 基本配置 + print(station_resource) + self.deck_config = station_resource + + # PLR 物料系统 + self.deck: Optional[Deck] = None + self.plr_resources: Dict[str, PLRResource] = {} + + # 资源同步器(可选) + # self.resource_synchronizer = ResourceSynchronizer(self) # 要在driver中自行初始化,只有workstation用 + + # 硬件接口 + self.hardware_interface: Union[Any, str] = None + + # 工作流状态 + self.current_workflow_status = WorkflowStatus.IDLE + self.current_workflow_info = None + self.workflow_start_time = None + self.workflow_parameters = {} + + # 支持的工作流(静态预定义) + self.supported_workflows: Dict[str, WorkflowInfo] = {} + + # 初始化物料系统 + self._initialize_material_system() + + # 注册支持的工作流 + # self._register_supported_workflows() + + # logger.info(f"工作站 {device_id} 初始化完成(简化版)") + + def _initialize_material_system(self): + """初始化物料系统 - 使用 graphio 转换""" + try: + from unilabos.resources.graphio import resource_ulab_to_plr + + # # 1. 合并 deck_config 和 children 创建完整的资源树 + # complete_resource_config = self._create_complete_resource_config() + + # # 2. 使用 graphio 转换为 PLR 资源 + # self.deck = resource_ulab_to_plr(complete_resource_config, plr_model=True) + + # # 3. 建立资源映射 + # self._build_resource_mappings(self.deck) + + # # 4. 如果有资源同步器,执行初始同步 + # if self.resource_synchronizer: + # # 这里可以异步执行,暂时跳过 + # pass + + # logger.info(f"工作站 {self.device_id} 物料系统初始化成功,创建了 {len(self.plr_resources)} 个资源") + pass + except Exception as e: + # logger.error(f"工作站 {self.device_id} 物料系统初始化失败: {e}") + raise + + def _create_complete_resource_config(self) -> Dict[str, Any]: + """创建完整的资源配置 - 合并 deck_config 和 children""" + # 创建主 deck 配置 + deck_resource = { + "id": f"{self.device_id}_deck", + "name": f"{self.device_id}_deck", + "type": "deck", + "position": {"x": 0, "y": 0, "z": 0}, + "config": { + "size_x": self.deck_config.get("size_x", 1000.0), + "size_y": self.deck_config.get("size_y", 1000.0), + "size_z": self.deck_config.get("size_z", 100.0), + **{k: v for k, v in self.deck_config.items() if k not in ["size_x", "size_y", "size_z"]}, + }, + "data": {}, + "children": [], + "parent": None, + } + + # 添加子资源 + if self._children: + children_list = [] + for child_id, child_config in self._children.items(): + child_resource = self._normalize_child_resource(child_id, child_config, deck_resource["id"]) + children_list.append(child_resource) + deck_resource["children"] = children_list + + return deck_resource + + 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} + + def _build_resource_mappings(self, deck: Deck): + """递归构建资源映射""" + + def add_resource_recursive(resource: PLRResource): + if hasattr(resource, "name"): + self.plr_resources[resource.name] = resource + + if hasattr(resource, "children"): + for child in resource.children: + add_resource_recursive(child) + + add_resource_recursive(deck) + + # ============ 硬件接口管理 ============ + + def set_hardware_interface(self, hardware_interface: Union[Any, str]): + """设置硬件接口""" + self.hardware_interface = hardware_interface + logger.info(f"工作站 {self.device_id} 硬件接口设置: {type(hardware_interface).__name__}") + + def set_workstation_node(self, workstation_node: "ROS2WorkstationNode"): + """设置协议节点引用(用于代理模式)""" + self._ros_node = workstation_node + logger.info(f"工作站 {self.device_id} 关联协议节点") + + # ============ 设备操作接口 ============ + + def call_device_method(self, method: str, *args, **kwargs) -> Any: + """调用设备方法的统一接口""" + # 1. 代理模式:通过协议节点转发 + if isinstance(self.hardware_interface, str) and self.hardware_interface.startswith("proxy:"): + if not self._ros_node: + raise RuntimeError("代理模式需要设置workstation_node") + + device_id = self.hardware_interface[6:] # 移除 "proxy:" 前缀 + return self._ros_node.call_device_method(device_id, method, *args, **kwargs) + + # 2. 直接模式:直接调用硬件接口方法 + elif self.hardware_interface and hasattr(self.hardware_interface, method): + return getattr(self.hardware_interface, method)(*args, **kwargs) + + else: + raise AttributeError(f"硬件接口不支持方法: {method}") + + def get_device_status(self) -> Dict[str, Any]: + """获取设备状态""" + try: + return self.call_device_method("get_status") + except AttributeError: + # 如果设备不支持get_status方法,返回基础状态 + return { + "status": "unknown", + "interface_type": type(self.hardware_interface).__name__, + "timestamp": time.time(), + } + + def is_device_available(self) -> bool: + """检查设备是否可用""" + try: + self.get_device_status() + return True + except: + return False + + # ============ 物料系统接口 ============ + + def get_deck(self) -> Deck: + """获取主 Deck""" + return self.deck + + def get_all_resources(self) -> Dict[str, PLRResource]: + """获取所有 PLR 资源""" + return self.plr_resources.copy() + + def find_resource_by_name(self, name: str) -> Optional[PLRResource]: + """按名称查找资源""" + return self.plr_resources.get(name) + + def find_resources_by_type(self, resource_type: type) -> List[PLRResource]: + """按类型查找资源""" + return [res for res in self.plr_resources.values() if isinstance(res, resource_type)] + + async def sync_with_external_system(self) -> bool: + """与外部物料系统同步""" + if not self.resource_synchronizer: + logger.info(f"工作站 {self.device_id} 没有配置资源同步器") + return True + + try: + success = await self.resource_synchronizer.sync_from_external() + if success: + logger.info(f"工作站 {self.device_id} 外部同步成功") + else: + logger.warning(f"工作站 {self.device_id} 外部同步失败") + return success + except Exception as e: + logger.error(f"工作站 {self.device_id} 外部同步异常: {e}") + return False + + # ============ 简化的工作流控制 ============ + + def execute_workflow(self, workflow_name: str, parameters: Dict[str, Any]) -> bool: + """执行工作流""" + try: + # 设置工作流状态 + self.current_workflow_status = WorkflowStatus.INITIALIZING + self.workflow_parameters = parameters + self.workflow_start_time = time.time() + + # 委托给子类实现 + success = self._execute_workflow_impl(workflow_name, parameters) + + if success: + self.current_workflow_status = WorkflowStatus.RUNNING + logger.info(f"工作站 {self.device_id} 工作流 {workflow_name} 启动成功") + else: + self.current_workflow_status = WorkflowStatus.ERROR + logger.error(f"工作站 {self.device_id} 工作流 {workflow_name} 启动失败") + + return success + + except Exception as e: + self.current_workflow_status = WorkflowStatus.ERROR + logger.error(f"工作站 {self.device_id} 执行工作流失败: {e}") + return False + + def stop_workflow(self, emergency: bool = False) -> bool: + """停止工作流""" + try: + if self.current_workflow_status in [WorkflowStatus.IDLE, WorkflowStatus.STOPPED]: + logger.warning(f"工作站 {self.device_id} 没有正在运行的工作流") + return True + + self.current_workflow_status = WorkflowStatus.STOPPING + + # 委托给子类实现 + success = self._stop_workflow_impl(emergency) + + if success: + self.current_workflow_status = WorkflowStatus.STOPPED + logger.info(f"工作站 {self.device_id} 工作流停止成功 (紧急: {emergency})") + else: + self.current_workflow_status = WorkflowStatus.ERROR + logger.error(f"工作站 {self.device_id} 工作流停止失败") + + return success + + except Exception as e: + self.current_workflow_status = WorkflowStatus.ERROR + logger.error(f"工作站 {self.device_id} 停止工作流失败: {e}") + return False + + # ============ 状态属性 ============ + + @property + def workflow_status(self) -> WorkflowStatus: + """获取当前工作流状态""" + return self.current_workflow_status + + @property + def is_busy(self) -> bool: + """检查工作站是否忙碌""" + return self.current_workflow_status in [ + WorkflowStatus.INITIALIZING, + WorkflowStatus.RUNNING, + WorkflowStatus.STOPPING, + ] + + @property + def workflow_runtime(self) -> float: + """获取工作流运行时间(秒)""" + if self.workflow_start_time is None: + return 0.0 + return time.time() - self.workflow_start_time + + # ============ 抽象方法 - 子类必须实现 ============ + + # @abstractmethod + # def _register_supported_workflows(self): + # """注册支持的工作流 - 子类必须实现""" + # pass + + # @abstractmethod + # def _execute_workflow_impl(self, workflow_name: str, parameters: Dict[str, Any]) -> bool: + # """执行工作流的具体实现 - 子类必须实现""" + # pass + + # @abstractmethod + # def _stop_workflow_impl(self, emergency: bool = False) -> bool: + # """停止工作流的具体实现 - 子类必须实现""" + # pass + +class WorkstationExample(WorkstationBase): + """工作站示例实现""" + + def _register_supported_workflows(self): + """注册支持的工作流""" + self.supported_workflows["example_workflow"] = WorkflowInfo( + name="example_workflow", + description="这是一个示例工作流", + estimated_duration=300.0, + required_materials=["sample_plate"], + output_product="processed_plate", + parameters_schema={"param1": "string", "param2": "integer"}, + ) + + def _execute_workflow_impl(self, workflow_name: str, parameters: Dict[str, Any]) -> bool: + """执行工作流的具体实现""" + if workflow_name not in self.supported_workflows: + logger.error(f"工作站 {self.device_id} 不支持工作流: {workflow_name}") + return False + + # 这里添加实际的工作流逻辑 + logger.info(f"工作站 {self.device_id} 正在执行工作流: {workflow_name} with parameters {parameters}") + return True + + def _stop_workflow_impl(self, emergency: bool = False) -> bool: + """停止工作流的具体实现""" + # 这里添加实际的停止逻辑 + logger.info(f"工作站 {self.device_id} 正在停止工作流 (紧急: {emergency})") + return True \ No newline at end of file diff --git a/unilabos/registry/devices/work_station.yaml b/unilabos/registry/devices/work_station.yaml index 380c3f64..ba076917 100644 --- a/unilabos/registry/devices/work_station.yaml +++ b/unilabos/registry/devices/work_station.yaml @@ -1,3 +1,517 @@ +bettery_station_registry: + category: + - work_station + class: + action_value_mappings: + auto-change_hole_sheet_to_2: + feedback: {} + goal: {} + goal_default: + hole: null + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + hole: + type: object + required: + - hole + type: object + result: {} + required: + - goal + title: change_hole_sheet_to_2参数 + type: object + type: UniLabJsonCommandAsync + auto-fill_plate: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: fill_plate参数 + type: object + type: UniLabJsonCommandAsync + auto-fun_wuliao_test: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: fun_wuliao_test参数 + type: object + type: UniLabJsonCommand + auto-func_allpack_cmd: + feedback: {} + goal: {} + goal_default: + assembly_pressure: 4200 + assembly_type: 7 + elec_num: null + elec_use_num: null + elec_vol: 50 + file_path: D:\coin_cell_data + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + assembly_pressure: + default: 4200 + type: integer + assembly_type: + default: 7 + type: integer + elec_num: + type: string + elec_use_num: + type: string + elec_vol: + default: 50 + type: integer + file_path: + default: D:\coin_cell_data + type: string + required: + - elec_num + - elec_use_num + type: object + result: {} + required: + - goal + title: func_allpack_cmd参数 + type: object + type: UniLabJsonCommand + auto-func_get_csv_export_status: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: func_get_csv_export_status参数 + type: object + type: UniLabJsonCommand + auto-func_pack_device_auto: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: func_pack_device_auto参数 + type: object + type: UniLabJsonCommand + auto-func_pack_device_init: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: func_pack_device_init参数 + type: object + type: UniLabJsonCommand + auto-func_pack_device_start: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: func_pack_device_start参数 + type: object + type: UniLabJsonCommand + auto-func_pack_device_stop: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: func_pack_device_stop参数 + type: object + type: UniLabJsonCommand + auto-func_pack_get_msg_cmd: + feedback: {} + goal: {} + goal_default: + file_path: D:\coin_cell_data + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + file_path: + default: D:\coin_cell_data + type: string + required: [] + type: object + result: {} + required: + - goal + title: func_pack_get_msg_cmd参数 + type: object + type: UniLabJsonCommand + auto-func_pack_send_bottle_num: + feedback: {} + goal: {} + goal_default: + bottle_num: null + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + bottle_num: + type: string + required: + - bottle_num + type: object + result: {} + required: + - goal + title: func_pack_send_bottle_num参数 + type: object + type: UniLabJsonCommand + auto-func_pack_send_finished_cmd: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: func_pack_send_finished_cmd参数 + type: object + type: UniLabJsonCommand + auto-func_pack_send_msg_cmd: + feedback: {} + goal: {} + goal_default: + assembly_pressure: null + assembly_type: null + elec_use_num: null + elec_vol: null + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + assembly_pressure: + type: string + assembly_type: + type: string + elec_use_num: + type: string + elec_vol: + type: string + required: + - elec_use_num + - elec_vol + - assembly_type + - assembly_pressure + type: object + result: {} + required: + - goal + title: func_pack_send_msg_cmd参数 + type: object + type: UniLabJsonCommand + auto-func_read_data_and_output: + feedback: {} + goal: {} + goal_default: + file_path: D:\coin_cell_data + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + file_path: + default: D:\coin_cell_data + type: string + required: [] + type: object + result: {} + required: + - goal + title: func_read_data_and_output参数 + type: object + type: UniLabJsonCommand + auto-func_stop_read_data: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: {} + required: + - goal + title: func_stop_read_data参数 + type: object + type: UniLabJsonCommand + auto-modify_deck_name: + feedback: {} + goal: {} + goal_default: + resource_name: null + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + resource_name: + type: string + required: + - resource_name + type: object + result: {} + required: + - goal + title: modify_deck_name参数 + type: object + type: UniLabJsonCommand + auto-post_init: + feedback: {} + goal: {} + goal_default: + ros_node: null + handles: {} + result: {} + schema: + description: '' + properties: + feedback: {} + goal: + properties: + ros_node: + type: object + required: + - ros_node + type: object + result: {} + required: + - goal + title: post_init参数 + type: object + type: UniLabJsonCommand + module: unilabos.devices.workstation.coin_cell_assembly.coin_cell_assembly:CoinCellAssemblyWorkstation + status_types: + data_assembly_coin_cell_num: int + data_assembly_pressure: int + data_assembly_time: float + data_axis_x_pos: float + data_axis_y_pos: float + data_axis_z_pos: float + data_coin_cell_code: str + data_coin_num: int + data_electrolyte_code: str + data_electrolyte_volume: int + data_glove_box_o2_content: float + data_glove_box_pressure: float + data_glove_box_water_content: float + data_open_circuit_voltage: float + data_pole_weight: float + request_rec_msg_status: bool + request_send_msg_status: bool + sys_mode: str + sys_status: str + type: python + config_info: [] + description: '' + handles: [] + icon: '' + init_param_schema: + config: + properties: + address: + default: 192.168.1.20 + type: string + debug_mode: + default: true + type: boolean + port: + default: '502' + type: string + station_resource: + type: object + required: + - station_resource + type: object + data: + properties: + data_assembly_coin_cell_num: + type: integer + data_assembly_pressure: + type: integer + data_assembly_time: + type: number + data_axis_x_pos: + type: number + data_axis_y_pos: + type: number + data_axis_z_pos: + type: number + data_coin_cell_code: + type: string + data_coin_num: + type: integer + data_electrolyte_code: + type: string + data_electrolyte_volume: + type: integer + data_glove_box_o2_content: + type: number + data_glove_box_pressure: + type: number + data_glove_box_water_content: + type: number + data_open_circuit_voltage: + type: number + data_pole_weight: + type: number + request_rec_msg_status: + type: boolean + request_send_msg_status: + type: boolean + sys_mode: + type: string + sys_status: + type: string + required: + - sys_status + - sys_mode + - request_rec_msg_status + - request_send_msg_status + - data_assembly_coin_cell_num + - data_assembly_time + - data_open_circuit_voltage + - data_axis_x_pos + - data_axis_y_pos + - data_axis_z_pos + - data_pole_weight + - data_assembly_pressure + - data_electrolyte_volume + - data_coin_num + - data_coin_cell_code + - data_electrolyte_code + - data_glove_box_pressure + - data_glove_box_o2_content + - data_glove_box_water_content + type: object + version: 1.0.0 workstation: category: - work_station @@ -6024,9 +6538,97 @@ workstation: title: WashSolid type: object type: WashSolid - module: unilabos.devices.workstation.workstation_base:ProtocolNode + auto-create_ros_action_server: + feedback: {} + goal: {} + goal_default: + action_name: null + action_value_mapping: null + handles: {} + result: {} + schema: + description: create_ros_action_server的参数schema + properties: + feedback: {} + goal: + properties: + action_name: + type: string + action_value_mapping: + type: string + required: + - action_name + - action_value_mapping + type: object + result: {} + required: + - goal + title: create_ros_action_server参数 + type: object + type: UniLabJsonCommand + auto-execute_single_action: + feedback: {} + goal: {} + goal_default: + action_kwargs: null + action_name: null + device_id: null + handles: {} + result: {} + schema: + description: execute_single_action的参数schema + properties: + feedback: {} + goal: + properties: + action_kwargs: + type: string + action_name: + type: string + device_id: + type: string + required: + - device_id + - action_name + - action_kwargs + type: object + result: {} + required: + - goal + title: execute_single_action参数 + type: object + type: UniLabJsonCommandAsync + auto-initialize_device: + feedback: {} + goal: {} + goal_default: + device_config: null + device_id: null + handles: {} + result: {} + schema: + description: initialize_device的参数schema + properties: + feedback: {} + goal: + properties: + device_config: + type: string + device_id: + type: string + required: + - device_id + - device_config + type: object + result: {} + required: + - goal + title: initialize_device参数 + type: object + type: UniLabJsonCommand + module: unilabos.ros.nodes.presets.workstation:ROS2WorkstationNode status_types: {} - type: python + type: ros2 config_info: [] description: Workstation handles: [] @@ -6034,437 +6636,63 @@ workstation: init_param_schema: config: properties: - deck: + action_value_mappings: + type: object + children: + type: object + device_id: type: string + driver_instance: + type: string + hardware_interface: + type: object + print_publish: + default: true + type: string + protocol_type: + items: + type: string + type: array + resource_tracker: + type: string + status_types: + type: object required: - - deck + - protocol_type + - children + - driver_instance + - device_id + - status_types + - action_value_mappings + - hardware_interface type: object data: properties: {} required: [] type: object version: 1.0.0 -workstation.bioyond: +workstation.example: category: - work_station class: - action_value_mappings: - auto-append_to_workflow_sequence: - feedback: {} - goal: {} - goal_default: - web_workflow_name: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - web_workflow_name: - type: string - required: - - web_workflow_name - type: object - result: {} - required: - - goal - title: append_to_workflow_sequence参数 - type: object - type: UniLabJsonCommand - auto-clear_workflows: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: clear_workflows参数 - type: object - type: UniLabJsonCommand - auto-load_bioyond_data_from_file: - feedback: {} - goal: {} - goal_default: - file_path: null - handles: {} - placeholder_keys: {} - 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 - auto-post_init: - feedback: {} - goal: {} - goal_default: - ros_node: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - ros_node: - type: object - required: - - ros_node - type: object - result: {} - required: - - goal - title: post_init参数 - type: object - type: UniLabJsonCommand - auto-process_web_workflows: - feedback: {} - goal: {} - goal_default: - json_str: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - json_str: - type: string - required: - - json_str - type: object - result: {} - required: - - goal - title: process_web_workflows参数 - type: object - type: UniLabJsonCommand - auto-reset_workstation: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: reset_workstation参数 - type: object - type: UniLabJsonCommand - auto-resource_tree_add: - feedback: {} - goal: {} - goal_default: - resources: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - resources: - items: - type: object - type: array - required: - - resources - type: object - result: {} - required: - - goal - title: resource_tree_add参数 - type: object - type: UniLabJsonCommand - auto-set_workflow_sequence: - feedback: {} - goal: {} - goal_default: - json_str: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - json_str: - type: string - required: - - json_str - type: object - result: {} - required: - - goal - title: set_workflow_sequence参数 - type: object - type: UniLabJsonCommand - auto-transfer_resource_to_another: - feedback: {} - goal: {} - goal_default: - mount_device_id: null - mount_resource: null - resource: null - sites: null - handles: {} - placeholder_keys: - mount_device_id: unilabos_devices - mount_resource: unilabos_resources - resource: unilabos_resources - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - mount_device_id: - type: object - mount_resource: - 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: mount_resource - type: object - type: array - resource: - 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: resource - type: object - type: array - sites: - items: - type: string - type: array - required: - - resource - - mount_resource - - sites - - mount_device_id - type: object - result: {} - required: - - goal - title: transfer_resource_to_another参数 - type: object - type: UniLabJsonCommand - module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation - status_types: - all_workflows: dict - bioyond_status: dict - station_info: dict - workstation_status: dict + action_value_mappings: {} + module: unilabos.devices.workstation.workstation_base:WorkstationExample + status_types: {} type: python config_info: [] description: '' handles: [] - icon: 反应站.webp + icon: '' init_param_schema: config: properties: - bioyond_config: - type: string - deck: - type: string - required: [] - type: object - data: - properties: - all_workflows: - type: object - bioyond_status: - type: object - station_info: - type: object - workstation_status: + station_resource: type: object required: - - bioyond_status - - all_workflows - - station_info - - workstation_status + - station_resource + type: object + data: + properties: {} + required: [] type: object version: 1.0.0 diff --git a/unilabos/registry/resources/bioyond/bottles.yaml b/unilabos/registry/resources/bioyond/bottles.yaml index 556b5f8b..b8865100 100644 --- a/unilabos/registry/resources/bioyond/bottles.yaml +++ b/unilabos/registry/resources/bioyond/bottles.yaml @@ -8,3 +8,144 @@ YB_jia_yang_tou_da: icon: '' init_param_schema: {} version: 1.0.0 + +YB_Liquid_Vial: + category: + - bottles + class: + module: unilabos.resources.bioyond.bottles:YB_Liquid_Vial + type: pylabrobot + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 +YB_Reagent_Bottle: + category: + - bottles + class: + module: unilabos.resources.bioyond.bottles:YB_Reagent_Bottle + type: pylabrobot + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 +YB_Solid_Stock: + category: + - bottles + class: + module: unilabos.resources.bioyond.bottles:YB_Solid_Stock + type: pylabrobot + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 +YB_Solid_Vial: + category: + - bottles + class: + module: unilabos.resources.bioyond.bottles:YB_Solid_Vial + type: pylabrobot + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 +YB_Solution_Beaker: + category: + - bottles + class: + module: unilabos.resources.bioyond.bottles:YB_Solution_Beaker + type: pylabrobot + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 +YB_100ml_Liquid_Bottle: + category: + - yb3 + class: + module: unilabos.resources.bioyond.bottles:YB_100ml_Liquid_Bottle + type: pylabrobot + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 +YB_Liquid_Bottle: + category: + - yb3 + class: + module: unilabos.resources.bioyond.bottles:YB_Liquid_Bottle + type: pylabrobot + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 +YB_High_Viscosity_Liquid_Bottle: + category: + - yb3 + class: + module: unilabos.resources.bioyond.bottles:YB_High_Viscosity_Liquid_Bottle + type: pylabrobot + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 +YB_Large_Dispense_Head: + category: + - yb3 + class: + module: unilabos.resources.bioyond.bottles:YB_Large_Dispense_Head + type: pylabrobot + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 +YB_5ml_Dispensing_Vial: + category: + - yb3 + class: + module: unilabos.resources.bioyond.bottles:YB_5ml_Dispensing_Vial + type: pylabrobot + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 +YB_20ml_Dispensing_Vial: + category: + - yb3 + class: + module: unilabos.resources.bioyond.bottles:YB_20ml_Dispensing_Vial + type: pylabrobot + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 +YB_Small_Solution_Bottle: + category: + - yb3 + class: + module: unilabos.resources.bioyond.bottles:YB_Small_Solution_Bottle + type: pylabrobot + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 +YB_Large_Solution_Bottle: + category: + - yb3 + class: + module: unilabos.resources.bioyond.bottles:YB_Large_Solution_Bottle + type: pylabrobot + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 +YB_Pipette_Tip: + category: + - yb3 + class: + module: unilabos.resources.bioyond.bottles:YB_Pipette_Tip + type: pylabrobot + handles: [] + icon: '' + init_param_schema: {} + version: 1.0.0 diff --git a/unilabos/resources/bioyond/bottle_carriers.py b/unilabos/resources/bioyond/bottle_carriers.py new file mode 100644 index 00000000..76e3a935 --- /dev/null +++ b/unilabos/resources/bioyond/bottle_carriers.py @@ -0,0 +1,620 @@ +from pylabrobot.resources import create_homogeneous_resources, Coordinate, ResourceHolder, create_ordered_items_2d + +from unilabos.resources.itemized_carrier import Bottle, BottleCarrier +from unilabos.resources.bioyond.bottles import ( + YB_Solid_Stock, + YB_Solid_Vial, + YB_Liquid_Vial, + YB_Solution_Beaker, + YB_Reagent_Bottle, + YB_5ml_Dispensing_Vial, + YB_20ml_Dispensing_Vial, + YB_Small_Solution_Bottle, + YB_Large_Solution_Bottle, + YB_Large_Dispense_Head, + YB_Pipette_Tip +) +# 命名约定:试剂瓶-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 + + 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=sites, + model="Electrolyte_6VialCarrier", + ) + carrier.num_items_x = 3 + carrier.num_items_y = 2 + carrier.num_items_z = 1 + for i in range(6): + carrier[i] = YB_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="Electrolyte_1BottleCarrier", + ) + carrier.num_items_x = 1 + carrier.num_items_y = 1 + carrier.num_items_z = 1 + carrier[0] = YB_Solution_Beaker(f"{name}_beaker_1") + return carrier + + +def YB_6StockCarrier(name: str) -> BottleCarrier: + """6瓶载架 - 2x3布局""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 50.0 + + # 瓶位尺寸 + bottle_diameter = 20.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 + + 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=sites, + model="6StockCarrier", + ) + 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] = YB_Solid_Stock(f"{name}_vial_{ordering[i]}") + return carrier + + +def YB_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 + + 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=sites, + model="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(3): + carrier[i] = YB_Solid_Vial(f"{name}_solidvial_{ordering[i]}") + for i in range(3, 6): + carrier[i] = YB_Liquid_Vial(f"{name}_liquidvial_{ordering[i]}") + return carrier + + +def YB_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="1BottleCarrier", + ) + carrier.num_items_x = 1 + carrier.num_items_y = 1 + carrier.num_items_z = 1 + carrier[0] = YB_Reagent_Bottle(f"{name}_flask_1") + return carrier + + +def YB_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="1FlaskCarrier", + ) + carrier.num_items_x = 1 + carrier.num_items_y = 1 + carrier.num_items_z = 1 + carrier[0] = YB_Reagent_Bottle(f"{name}_bottle_1") + return carrier + + +def YB_6x5ml_DispensingVialCarrier(name: str) -> BottleCarrier: + """5ml分液瓶板 - 4x2布局,8个位置""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 50.0 + + # 瓶位尺寸 + bottle_diameter = 15.0 + bottle_spacing_x = 42.0 # X方向间距 + bottle_spacing_y = 35.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (4 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=4, + 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=sites, + model="6x5ml_DispensingVialCarrier", + ) + carrier.num_items_x = 4 + carrier.num_items_y = 2 + carrier.num_items_z = 1 + ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"] + for i in range(8): + carrier[i] = YB_5ml_Dispensing_Vial(f"{name}_vial_{ordering[i]}") + return carrier + + +def YB_6x20ml_DispensingVialCarrier(name: str) -> BottleCarrier: + """20ml分液瓶板 - 4x2布局,8个位置""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 70.0 + + # 瓶位尺寸 + bottle_diameter = 20.0 + bottle_spacing_x = 42.0 # X方向间距 + bottle_spacing_y = 35.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (4 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=4, + 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=sites, + model="6x20ml_DispensingVialCarrier", + ) + carrier.num_items_x = 4 + carrier.num_items_y = 2 + carrier.num_items_z = 1 + ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"] + for i in range(8): + carrier[i] = YB_20ml_Dispensing_Vial(f"{name}_vial_{ordering[i]}") + return carrier + + +def YB_6x_SmallSolutionBottleCarrier(name: str) -> BottleCarrier: + """配液瓶(小)板 - 4x2布局,8个位置""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 65.0 + + # 瓶位尺寸 + bottle_diameter = 35.0 + bottle_spacing_x = 42.0 # X方向间距 + bottle_spacing_y = 35.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (4 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=4, + 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=sites, + model="6x_SmallSolutionBottleCarrier", + ) + carrier.num_items_x = 4 + carrier.num_items_y = 2 + carrier.num_items_z = 1 + ordering = ["A1", "A2", "A3", "A4", "B1", "B2", "B3", "B4"] + for i in range(8): + carrier[i] = YB_Small_Solution_Bottle(f"{name}_bottle_{ordering[i]}") + return carrier + + +def YB_4x_LargeSolutionBottleCarrier(name: str) -> BottleCarrier: + """配液瓶(大)板 - 2x2布局,4个位置""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 95.0 + + # 瓶位尺寸 + bottle_diameter = 55.0 + bottle_spacing_x = 60.0 # X方向间距 + bottle_spacing_y = 60.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (2 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (2 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=2, + 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=sites, + model="4x_LargeSolutionBottleCarrier", + ) + carrier.num_items_x = 2 + carrier.num_items_y = 2 + carrier.num_items_z = 1 + ordering = ["A1", "A2", "B1", "B2"] + for i in range(4): + carrier[i] = YB_Large_Solution_Bottle(f"{name}_bottle_{ordering[i]}") + return carrier + + +def YB_6x_LargeDispenseHeadCarrier(name: str) -> BottleCarrier: + """加样头(大)板 - 1x1布局,1个位置""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 95.0 + + # 瓶位尺寸 + bottle_diameter = 35.0 + bottle_spacing_x = 42.0 # X方向间距 + bottle_spacing_y = 35.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (1 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (1 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=1, + num_items_y=1, + 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=sites, + model="6x_LargeDispenseHeadCarrier", + ) + carrier.num_items_x = 1 + carrier.num_items_y = 1 + carrier.num_items_z = 1 + carrier[0] = YB_Large_Dispense_Head(f"{name}_head_1") + return carrier + + +def YB_AdapterBlock(name: str) -> BottleCarrier: + """适配器块 - 单个中央位置""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 30.0 + + # 适配器尺寸 + adapter_diameter = 80.0 + + # 计算中央位置 + center_x = (carrier_size_x - adapter_diameter) / 2 + center_y = (carrier_size_y - adapter_diameter) / 2 + center_z = 0.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=adapter_diameter, + resource_size_y=adapter_diameter, + name_prefix=name, + ), + model="AdapterBlock", + ) + carrier.num_items_x = 1 + carrier.num_items_y = 1 + carrier.num_items_z = 1 + # 适配器块本身不包含瓶子,只是一个支撑结构 + return carrier + + +def YB_TipBox(name: str) -> BottleCarrier: + """枪头盒 - 8x12布局,96个位置""" + + # 载架尺寸 (mm) + carrier_size_x = 127.8 + carrier_size_y = 85.5 + carrier_size_z = 55.0 + + # 枪头尺寸 + tip_diameter = 10.0 + tip_spacing_x = 9.0 # X方向间距 + tip_spacing_y = 9.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (12 - 1) * tip_spacing_x - tip_diameter) / 2 + start_y = (carrier_size_y - (8 - 1) * tip_spacing_y - tip_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=12, + num_items_y=8, + dx=start_x, + dy=start_y, + dz=5.0, + item_dx=tip_spacing_x, + item_dy=tip_spacing_y, + size_x=tip_diameter, + size_y=tip_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=sites, + model="TipBox", + ) + carrier.num_items_x = 12 + carrier.num_items_y = 8 + carrier.num_items_z = 1 + # 创建96个枪头 + for i in range(96): + row = chr(65 + i // 12) # A-H + col = (i % 12) + 1 # 1-12 + carrier[i] = YB_Pipette_Tip(f"{name}_tip_{row}{col}") + return carrier + diff --git a/unilabos/resources/bioyond/bottles.py b/unilabos/resources/bioyond/bottles.py new file mode 100644 index 00000000..23b9523e --- /dev/null +++ b/unilabos/resources/bioyond/bottles.py @@ -0,0 +1,255 @@ +from unilabos.resources.itemized_carrier import Bottle, BottleCarrier +# 工厂函数 + + +def YB_Solid_Stock( + name: str, + diameter: float = 20.0, + height: float = 100.0, + max_volume: float = 30000.0, # 30mL + barcode: str = None, +) -> Bottle: + """创建粉末瓶""" + return Bottle( + name=name, + diameter=diameter,# 未知 + height=height, + max_volume=max_volume, + barcode=barcode, + model="Solid_Stock", + ) + + +def YB_Solid_Vial( + name: str, + diameter: float = 25.0, + height: float = 60.0, + max_volume: float = 30000.0, # 30mL + barcode: str = None, +) -> Bottle: + """创建粉末瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="Solid_Vial", + ) + + +def YB_Liquid_Vial( + name: str, + diameter: float = 25.0, + height: float = 60.0, + max_volume: float = 30000.0, # 30mL + barcode: str = None, +) -> Bottle: + """创建滴定液瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="Liquid_Vial", + ) + + +def YB_Solution_Beaker( + name: str, + diameter: float = 60.0, + height: float = 70.0, + max_volume: float = 200000.0, # 200mL + barcode: str = None, +) -> Bottle: + """创建溶液烧杯""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="Solution_Beaker", + ) + + +def YB_Reagent_Bottle( + name: str, + diameter: float = 70.0, + height: float = 120.0, + max_volume: float = 500000.0, # 500mL + barcode: str = None, +) -> Bottle: + """创建试剂瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="Reagent_Bottle", + ) + + +def YB_100ml_Liquid_Bottle( + name: str, + diameter: float = 50.0, + height: float = 80.0, + max_volume: float = 100000.0, # 100mL + barcode: str = None, +) -> Bottle: + """创建100ml液体瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="100ml_Liquid_Bottle", + ) + + +def YB_Liquid_Bottle( + name: str, + diameter: float = 40.0, + height: float = 70.0, + max_volume: float = 50000.0, # 50mL + barcode: str = None, +) -> Bottle: + """创建液体瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="Liquid_Bottle", + ) + + +def YB_High_Viscosity_Liquid_Bottle( + name: str, + diameter: float = 45.0, + height: float = 75.0, + max_volume: float = 60000.0, # 60mL + barcode: str = None, +) -> Bottle: + """创建高粘液瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="High_Viscosity_Liquid_Bottle", + ) + + +def YB_Large_Dispense_Head( + name: str, + diameter: float = 35.0, + height: float = 90.0, + max_volume: float = 50000.0, # 50mL + barcode: str = None, +) -> Bottle: + """创建加样头(大)""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="Large_Dispense_Head", + ) + + +def YB_5ml_Dispensing_Vial( + name: str, + diameter: float = 15.0, + height: float = 45.0, + max_volume: float = 5000.0, # 5mL + barcode: str = None, +) -> Bottle: + """创建5ml分液瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="5ml_Dispensing_Vial", + ) + + +def YB_20ml_Dispensing_Vial( + name: str, + diameter: float = 20.0, + height: float = 65.0, + max_volume: float = 20000.0, # 20mL + barcode: str = None, +) -> Bottle: + """创建20ml分液瓶""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="20ml_Dispensing_Vial", + ) + + +def YB_Small_Solution_Bottle( + name: str, + diameter: float = 35.0, + height: float = 60.0, + max_volume: float = 40000.0, # 40mL + barcode: str = None, +) -> Bottle: + """创建配液瓶(小)""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="Small_Solution_Bottle", + ) + + +def YB_Large_Solution_Bottle( + name: str, + diameter: float = 55.0, + height: float = 90.0, + max_volume: float = 150000.0, # 150mL + barcode: str = None, +) -> Bottle: + """创建配液瓶(大)""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="Large_Solution_Bottle", + ) + + +def YB_Pipette_Tip( + name: str, + diameter: float = 10.0, + height: float = 50.0, + max_volume: float = 1000.0, # 1mL + barcode: str = None, +) -> Bottle: + """创建枪头""" + return Bottle( + name=name, + diameter=diameter, + height=height, + max_volume=max_volume, + barcode=barcode, + model="Pipette_Tip", + ) +