From 966b51042dd443a811b28e3329cf9420c60c551e Mon Sep 17 00:00:00 2001 From: Junhan Chang Date: Thu, 6 Nov 2025 00:59:46 +0800 Subject: [PATCH] =?UTF-8?q?rename=20and=20fix=20all=20Yihua=20Materials:?= =?UTF-8?q?=20ClipMagazineHole=E2=86=92Magazine(ResourceStack),=20and=20us?= =?UTF-8?q?e=20factory=20functions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bioyond_yihua_YB.json | 104 +-- new_cellconfig.json | 54 ++ .../bioyond_cell/bioyond_cell_workstation.py | 10 +- .../bioyond_cell/bioyond_yihua_YB.json | 104 +-- .../coin_cell_assembly/YB_YH_materials.py | 736 +++--------------- .../coin_cell_assembly/coin_cell_assembly.py | 17 +- .../workstation_material_management.py | 583 -------------- unilabos/registry/resources/bioyond/deck.yaml | 3 - unilabos/resources/battery/bottle_carriers.py | 56 ++ unilabos/resources/battery/magazine.py | 284 +++++++ unilabos/resources/itemized_carrier.py | 2 +- 11 files changed, 602 insertions(+), 1351 deletions(-) create mode 100644 new_cellconfig.json delete mode 100644 unilabos/devices/workstation/workstation_material_management.py create mode 100644 unilabos/resources/battery/bottle_carriers.py create mode 100644 unilabos/resources/battery/magazine.py diff --git a/bioyond_yihua_YB.json b/bioyond_yihua_YB.json index c38179d9..668ad6a2 100644 --- a/bioyond_yihua_YB.json +++ b/bioyond_yihua_YB.json @@ -99,7 +99,7 @@ "z": 0 }, "config": { - "type": "ClipMagazine_four", + "type": "MagazineHolder_4", "size_x": 80, "size_y": 80, "size_z": 10, @@ -140,7 +140,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -235,7 +235,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -330,7 +330,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -425,7 +425,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -523,7 +523,7 @@ "z": 0 }, "config": { - "type": "ClipMagazine_four", + "type": "MagazineHolder_4", "size_x": 80, "size_y": 80, "size_z": 10, @@ -564,7 +564,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -659,7 +659,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -754,7 +754,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -849,7 +849,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -949,7 +949,7 @@ "z": 0 }, "config": { - "type": "ClipMagazine", + "type": "MagazineHolder_6", "size_x": 80, "size_y": 80, "size_z": 10, @@ -992,7 +992,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -1087,7 +1087,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -1182,7 +1182,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -1277,7 +1277,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -1372,7 +1372,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -1467,7 +1467,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -1567,7 +1567,7 @@ "z": 0 }, "config": { - "type": "ClipMagazine", + "type": "MagazineHolder_6", "size_x": 80, "size_y": 80, "size_z": 10, @@ -1610,7 +1610,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -1705,7 +1705,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -1800,7 +1800,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -1895,7 +1895,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -1990,7 +1990,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -2085,7 +2085,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -2185,7 +2185,7 @@ "z": 0 }, "config": { - "type": "ClipMagazine", + "type": "MagazineHolder_6", "size_x": 80, "size_y": 80, "size_z": 10, @@ -2228,7 +2228,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -2323,7 +2323,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -2418,7 +2418,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -2513,7 +2513,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -2608,7 +2608,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -2703,7 +2703,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -2803,7 +2803,7 @@ "z": 0 }, "config": { - "type": "ClipMagazine", + "type": "MagazineHolder_6", "size_x": 80, "size_y": 80, "size_z": 10, @@ -2846,7 +2846,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -2941,7 +2941,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3036,7 +3036,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3131,7 +3131,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3226,7 +3226,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3321,7 +3321,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3421,7 +3421,7 @@ "z": 0 }, "config": { - "type": "ClipMagazine", + "type": "MagazineHolder_6", "size_x": 80, "size_y": 80, "size_z": 10, @@ -3464,7 +3464,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3559,7 +3559,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3654,7 +3654,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3749,7 +3749,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3844,7 +3844,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3939,7 +3939,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -4039,7 +4039,7 @@ "z": 0 }, "config": { - "type": "ClipMagazine", + "type": "MagazineHolder_6", "size_x": 80, "size_y": 80, "size_z": 10, @@ -4082,7 +4082,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -4177,7 +4177,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -4272,7 +4272,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -4367,7 +4367,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -4462,7 +4462,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -4557,7 +4557,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, diff --git a/new_cellconfig.json b/new_cellconfig.json new file mode 100644 index 00000000..d06fd0eb --- /dev/null +++ b/new_cellconfig.json @@ -0,0 +1,54 @@ +{ + "nodes": [ + { + "id": "BatteryStation", + "name": "扣电工作站", + "parent": null, + "children": [ + "coin_cell_deck" + ], + "type": "device", + "class":"coincellassemblyworkstation_device", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "deck": { + "data": { + "_resource_child_name": "YB_YH_Deck", + "_resource_type": "unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:CoincellDeck" + } + }, + "debug_mode": true, + "protocol_type": [] + } + }, + { + "id": "YB_YH_Deck", + "name": "YB_YH_Deck", + "children": [], + "parent": "BatteryStation", + "type": "deck", + "class": "CoincellDeck", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "CoincellDeck", + "setup": true, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + } + }, + "data": {} + } + ], + "links": [] +} \ No newline at end of file diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py index ee825791..a6722306 100644 --- a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py +++ b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_cell_workstation.py @@ -16,6 +16,7 @@ from unilabos.devices.workstation.bioyond_studio.config import ( API_CONFIG, MATERIAL_TYPE_MAPPINGS, WAREHOUSE_MAPPING, SOLID_LIQUID_MAPPINGS ) from unilabos.devices.workstation.workstation_http_service import WorkstationHTTPService +from unilabos.resources.bioyond.decks import BIOYOND_YB_Deck from unilabos.utils.log import logger from unilabos.registry.registry import lab_registry @@ -1074,17 +1075,12 @@ class BioyondCellWorkstation(BioyondWorkstation): if __name__ == "__main__": lab_registry.setup() - ws = BioyondCellWorkstation() + deck = BIOYOND_YB_Deck(setup=True) + ws = BioyondCellWorkstation(deck=deck) # ws.create_sample(name="test", board_type="配液瓶(小)板", bottle_type="配液瓶(小)", location_code="B01") # logger.info(ws.scheduler_stop()) # logger.info(ws.scheduler_start()) - # results = ws.create_materials(SOLID_LIQUID_MAPPINGS) - # for r in results: - # logger.info(r) - # 从CSV文件读取物料列表并批量创建入库 - # result = ws.create_and_inbound_materials() - # 继续后续流程 # logger.info(ws.auto_feeding4to3()) #搬运物料到3号箱 # # # 使用正斜杠或 Path 对象来指定文件路径 diff --git a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_yihua_YB.json b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_yihua_YB.json index 3d1b98af..3119d0bf 100644 --- a/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_yihua_YB.json +++ b/unilabos/devices/workstation/bioyond_studio/bioyond_cell/bioyond_yihua_YB.json @@ -113,7 +113,7 @@ "z": 0 }, "config": { - "type": "ClipMagazine_four", + "type": "MagazineHolder_4", "size_x": 80, "size_y": 80, "size_z": 10, @@ -154,7 +154,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -249,7 +249,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -344,7 +344,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -439,7 +439,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -537,7 +537,7 @@ "z": 0 }, "config": { - "type": "ClipMagazine_four", + "type": "MagazineHolder_4", "size_x": 80, "size_y": 80, "size_z": 10, @@ -578,7 +578,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -673,7 +673,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -768,7 +768,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -863,7 +863,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -963,7 +963,7 @@ "z": 0 }, "config": { - "type": "ClipMagazine", + "type": "MagazineHolder_6", "size_x": 80, "size_y": 80, "size_z": 10, @@ -1006,7 +1006,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -1101,7 +1101,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -1196,7 +1196,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -1291,7 +1291,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -1386,7 +1386,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -1481,7 +1481,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -1581,7 +1581,7 @@ "z": 0 }, "config": { - "type": "ClipMagazine", + "type": "MagazineHolder_6", "size_x": 80, "size_y": 80, "size_z": 10, @@ -1624,7 +1624,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -1719,7 +1719,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -1814,7 +1814,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -1909,7 +1909,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -2004,7 +2004,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -2099,7 +2099,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -2199,7 +2199,7 @@ "z": 0 }, "config": { - "type": "ClipMagazine", + "type": "MagazineHolder_6", "size_x": 80, "size_y": 80, "size_z": 10, @@ -2242,7 +2242,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -2337,7 +2337,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -2432,7 +2432,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -2527,7 +2527,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -2622,7 +2622,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -2717,7 +2717,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -2817,7 +2817,7 @@ "z": 0 }, "config": { - "type": "ClipMagazine", + "type": "MagazineHolder_6", "size_x": 80, "size_y": 80, "size_z": 10, @@ -2860,7 +2860,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -2955,7 +2955,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3050,7 +3050,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3145,7 +3145,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3240,7 +3240,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3335,7 +3335,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3435,7 +3435,7 @@ "z": 0 }, "config": { - "type": "ClipMagazine", + "type": "MagazineHolder_6", "size_x": 80, "size_y": 80, "size_z": 10, @@ -3478,7 +3478,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3573,7 +3573,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3668,7 +3668,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3763,7 +3763,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3858,7 +3858,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -3953,7 +3953,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -4053,7 +4053,7 @@ "z": 0 }, "config": { - "type": "ClipMagazine", + "type": "MagazineHolder_6", "size_x": 80, "size_y": 80, "size_z": 10, @@ -4096,7 +4096,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -4191,7 +4191,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -4286,7 +4286,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -4381,7 +4381,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -4476,7 +4476,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, @@ -4571,7 +4571,7 @@ "z": 10 }, "config": { - "type": "ClipMagazineHole", + "type": "Magazine", "size_x": 14.0, "size_y": 14.0, "size_z": 10.0, diff --git a/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py b/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py index 156ab6f8..8bb0a8de 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py +++ b/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py @@ -18,6 +18,9 @@ from pylabrobot.resources.tip_rack import TipRack, TipSpot from pylabrobot.resources.trash import Trash from pylabrobot.resources.utils import create_ordered_items_2d +from unilabos.resources.battery.magazine import MagazineHolder_1, MagazineHolder_2, MagazineHolder_4, MagazineHolder_6 +from unilabos.resources.battery.bottle_carriers import YIHUA_Electrolyte_12VialCarrier + class ElectrodeSheetState(TypedDict): diameter: float # 直径 (mm) @@ -165,7 +168,6 @@ class MaterialHole(Resource): return self.children[index] - class MaterialPlateState(TypedDict): hole_spacing_x: float hole_spacing_y: float @@ -327,132 +329,6 @@ class PlateSlot(ResourceStack): } -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(ItemizedResource[ClipMagazineHole]): - """子弹夹类 - 有6个洞位,每个洞位放多个极片""" - children: List[ClipMagazineHole] - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - hole_diameter: float = 14.0, - hole_depth: float = 10.0, - hole_spacing: float = 25.0, - max_sheets_per_hole: int = 100, - category: str = "clip_magazine", - model: Optional[str] = None, - ): - """初始化子弹夹 - - Args: - name: 子弹夹名称 - size_x: 长度 (mm) - size_y: 宽度 (mm) - size_z: 高度 (mm) - hole_diameter: 洞直径 (mm) - hole_depth: 洞深度 (mm) - hole_spacing: 洞位间距 (mm) - max_sheets_per_hole: 每个洞位最大极片数量 - category: 类别 - model: 型号 - """ - # 创建6个洞位,排成2x3布局 - holes = create_ordered_items_2d( - klass=ClipMagazineHole, - num_items_x=3, - num_items_y=2, - dx=(size_x - 2 * hole_spacing) / 2, # 居中 - dy=(size_y - hole_spacing) / 2, # 居中 - dz=size_z - 0, - item_dx=hole_spacing, - item_dy=hole_spacing, - diameter=hole_diameter, - depth=hole_depth, - ) - - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - ordered_items=holes, - category=category, - model=model, - ) - - # 保存洞位的直径和深度 - self.hole_diameter = hole_diameter - self.hole_depth = hole_depth - self.max_sheets_per_hole = max_sheets_per_hole - - def serialize(self) -> dict: - return { - **super().serialize(), - "hole_diameter": self.hole_diameter, - "hole_depth": self.hole_depth, - "max_sheets_per_hole": self.max_sheets_per_hole, - } #是一种类型注解,不用self class BatteryState(TypedDict): """电池状态字典""" @@ -595,76 +471,54 @@ class BatteryPressSlot(Resource): 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, +def TipBox64( 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孔枪头盒 +): + """64孔枪头盒类""" + from pylabrobot.resources.tip import Tip - 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, + # 创建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=12, + 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, + ) + idx_available = list(range(0, 32)) + list(range(64, 96)) + tip_spots_available = {k: v for i, (k, v) in enumerate(tip_spots.items()) if i in idx_available} + return TipRack( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + ordered_items=tip_spots_available, + category=category, + model=model, + with_tips=True, + ) + class WasteTipBoxstate(TypedDict): @@ -682,8 +536,12 @@ class WasteTipBox(Trash): size_x: float = 127.8, size_y: float = 85.5, size_z: float = 60.0, - category: str = "waste_tip_box", - model: Optional[str] = None, + material_z_thickness=0, + max_volume=float("inf"), + category="trash", + model=None, + compute_volume_from_height=None, + compute_height_from_volume=None, ): """初始化废枪头盒 @@ -733,389 +591,6 @@ class WasteTipBox(Trash): 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[Resource] = [] - - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - category: str = "bottle_rack", - model: Optional[str] = None, - num_items_x: int = 2, - num_items_y: int = 4, - position_spacing: float = 35.0, - orientation: str = "horizontal", - padding_x: float = 20.0, - padding_y: float = 20.0, - ): - """初始化瓶架 - - Args: - name: 瓶架名称 - size_x: 长度 (mm) - size_y: 宽度 (mm) - size_z: 高度 (mm) - category: 类别 - model: 型号 - """ - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - category=category, - model=model, - ) - # 初始化状态 - self._unilabos_state: BottleRackState = BottleRackState( - bottle_diameter=30.0, - bottle_height=100.0, - position_spacing=position_spacing, - name_to_index={}, - ) - # 基于网格生成瓶位坐标映射(居中摆放) - # 使用内边距,避免点跑到容器外(前端渲染不按mm等比缩放时更稳妥) - origin_x = padding_x - origin_y = padding_y - self.index_to_pos = {} - for j in range(num_items_y): - for i in range(num_items_x): - idx = j * num_items_x + i - if orientation == "vertical": - # 纵向:沿 y 方向优先排列 - self.index_to_pos[idx] = Coordinate( - x=origin_x + j * position_spacing, - y=origin_y + i * position_spacing, - z=0, - ) - else: - # 横向(默认):沿 x 方向优先排列 - self.index_to_pos[idx] = Coordinate( - x=origin_x + i * position_spacing, - y=origin_y + j * position_spacing, - z=0, - ) - self.name_to_index = {} - self.name_to_pos = {} - self.num_items_x = num_items_x - self.num_items_y = num_items_y - self.orientation = orientation - self.padding_x = padding_x - self.padding_y = padding_y - - def load_state(self, state: Dict[str, Any]) -> None: - """格式不变""" - super().load_state(state) - self._unilabos_state = state - - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - """格式不变""" - data = super().serialize_state() - data.update( - self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - - # TODO: 这里有些问题要重新写一下 - def assign_child_resource_old(self, resource: Resource, location=Coordinate.zero(), reassign=True): - capacity = self.num_items_x * self.num_items_y - assert len(self.children) < capacity, "瓶架已满,无法添加更多瓶子" - index = len(self.children) - location = self.index_to_pos.get(index, Coordinate.zero()) - self.name_to_pos[resource.name] = location - self.name_to_index[resource.name] = index - return super().assign_child_resource(resource, location, reassign) - - def assign_child_resource(self, resource: Resource, index: int): - capacity = self.num_items_x * self.num_items_y - assert 0 <= index < capacity, "无效的瓶子索引" - self.name_to_index[resource.name] = index - location = self.index_to_pos[index] - return super().assign_child_resource(resource, location) - - def unassign_child_resource(self, resource: Bottle): - super().unassign_child_resource(resource) - self.index_to_pos.pop(self.name_to_index.pop(resource.name, None), None) - - def serialize(self) -> dict: - return { - **super().serialize(), - "num_items_x": self.num_items_x, - "num_items_y": self.num_items_y, - "position_spacing": self._unilabos_state.get("position_spacing", 35.0), - "orientation": self.orientation, - "padding_x": self.padding_x, - "padding_y": self.padding_y, - } - -class BottleState(TypedDict): - diameter: float - height: float - electrolyte_name: str - electrolyte_volume: float - max_volume: float - -class Bottle(Resource): - """瓶子类 - 容纳电解液""" - - def __init__( - self, - name: str, - category: str = "bottle", - ): - """初始化瓶子 - - Args: - name: 瓶子名称 - diameter: 直径 (mm) - height: 高度 (mm) - max_volume: 最大体积 (μL) - barcode: 二维码 - category: 类别 - model: 型号 - """ - super().__init__( - name=name, - size_x=1, - size_y=1, - size_z=1, - category=category, - ) - self._unilabos_state: BottleState = BottleState() - - def aspirate_electrolyte(self, volume: float) -> bool: - current_volume = self._unilabos_state["electrolyte_volume"] - assert current_volume > volume, f"Cannot aspirate {volume}μL, only {current_volume}μL available." - self._unilabos_state["electrolyte_volume"] -= volume - return True - - def load_state(self, state: Dict[str, Any]) -> None: - """格式不变""" - super().load_state(state) - self._unilabos_state = state - - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - """格式不变""" - data = super().serialize_state() - data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - -class ClipMagazine_four(ItemizedResource[ClipMagazineHole]): - """子弹夹类 - 有4个洞位,每个洞位放多个极片""" - children: List[ClipMagazineHole] - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - hole_diameter: float = 14.0, - hole_depth: float = 10.0, - hole_spacing: float = 25.0, - max_sheets_per_hole: int = 100, - category: str = "clip_magazine_four", - model: Optional[str] = None, - ): - """初始化子弹夹 - - Args: - name: 子弹夹名称 - size_x: 长度 (mm) - size_y: 宽度 (mm) - size_z: 高度 (mm) - hole_diameter: 洞直径 (mm) - hole_depth: 洞深度 (mm) - hole_spacing: 洞位间距 (mm) - max_sheets_per_hole: 每个洞位最大极片数量 - category: 类别 - model: 型号 - """ - # 创建4个洞位,排成2x2布局 - holes = create_ordered_items_2d( - klass=ClipMagazineHole, - num_items_x=2, - num_items_y=2, - dx=(size_x - 2 * hole_spacing) / 2, # 居中 - dy=(size_y - hole_spacing) / 2, # 居中 - dz=size_z - 0, - item_dx=hole_spacing, - item_dy=hole_spacing, - diameter=hole_diameter, - depth=hole_depth, - ) - - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - ordered_items=holes, - category=category, - model=model, - ) - - # 保存洞位的直径和深度 - self.hole_diameter = hole_diameter - self.hole_depth = hole_depth - self.max_sheets_per_hole = max_sheets_per_hole - - - def serialize(self) -> dict: - return { - **super().serialize(), - "hole_diameter": self.hole_diameter, - "hole_depth": self.hole_depth, - "max_sheets_per_hole": self.max_sheets_per_hole, - } - -class ClipMagazine_two(ItemizedResource[ClipMagazineHole]): - """子弹夹类 - 有2个洞位,每个洞位放多个极片""" - children: List[ClipMagazineHole] - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - hole_diameter: float = 14.0, - hole_depth: float = 10.0, - hole_spacing: float = 25.0, - max_sheets_per_hole: int = 100, - category: str = "clip_magazine_four", - model: Optional[str] = None, - ): - """初始化子弹夹 - - Args: - name: 子弹夹名称 - size_x: 长度 (mm) - size_y: 宽度 (mm) - size_z: 高度 (mm) - hole_diameter: 洞直径 (mm) - hole_depth: 洞深度 (mm) - hole_spacing: 洞位间距 (mm) - max_sheets_per_hole: 每个洞位最大极片数量 - category: 类别 - model: 型号 - """ - # 创建4个洞位,排成2x2布局 - holes = create_ordered_items_2d( - klass=ClipMagazineHole, - num_items_x=1, - num_items_y=2, - dx=(size_x - 2 * hole_spacing) / 2, # 居中 - dy=(size_y - hole_spacing) / 2, # 居中 - dz=size_z - 0, - item_dx=hole_spacing, - item_dy=hole_spacing, - diameter=hole_diameter, - depth=hole_depth, - ) - - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - ordered_items=holes, - category=category, - model=model, - ) - - # 保存洞位的直径和深度 - self.hole_diameter = hole_diameter - self.hole_depth = hole_depth - self.max_sheets_per_hole = max_sheets_per_hole - - - def serialize(self) -> dict: - return { - **super().serialize(), - "hole_diameter": self.hole_diameter, - "hole_depth": self.hole_depth, - "max_sheets_per_hole": self.max_sheets_per_hole, - } -class ClipMagazine_one(ItemizedResource[ClipMagazineHole]): - """子弹夹类 - 有1个洞位,每个洞位放多个极片""" - children: List[ClipMagazineHole] - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - hole_diameter: float = 14.0, - hole_depth: float = 10.0, - hole_spacing: float = 25.0, - max_sheets_per_hole: int = 100, - category: str = "clip_magazine_four", - model: Optional[str] = None, - ): - """初始化子弹夹 - - Args: - name: 子弹夹名称 - size_x: 长度 (mm) - size_y: 宽度 (mm) - size_z: 高度 (mm) - hole_diameter: 洞直径 (mm) - hole_depth: 洞深度 (mm) - hole_spacing: 洞位间距 (mm) - max_sheets_per_hole: 每个洞位最大极片数量 - category: 类别 - model: 型号 - """ - # 创建4个洞位,排成2x2布局 - holes = create_ordered_items_2d( - klass=ClipMagazineHole, - num_items_x=1, - num_items_y=1, - dx=(size_x - 2 * hole_spacing) / 2, # 居中 - dy=(size_y - hole_spacing) / 2, # 居中 - dz=size_z - 0, - item_dx=hole_spacing, - item_dy=hole_spacing, - diameter=hole_diameter, - depth=hole_depth, - ) - - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - ordered_items=holes, - category=category, - model=model, - ) - - # 保存洞位的直径和深度 - self.hole_diameter = hole_diameter - self.hole_depth = hole_depth - self.max_sheets_per_hole = max_sheets_per_hole - - - def serialize(self) -> dict: - return { - **super().serialize(), - "hole_diameter": self.hole_diameter, - "hole_depth": self.hole_depth, - "max_sheets_per_hole": self.max_sheets_per_hole, - } - class CoincellDeck(Deck): """纽扣电池组装工作站台面类""" @@ -1155,122 +630,96 @@ class CoincellDeck(Deck): """设置工作站的标准布局 - 包含子弹夹、料盘、瓶架等完整配置""" # ====================================== 子弹夹 ============================================ # 铝箔(1个洞位) - lvbo_zip = ClipMagazine_one("lvbo_zip", 80, 80, 10) + lvbo_zip = MagazineHolder_1("铝箔弹夹", 80, 80, 10) self.assign_child_resource(lvbo_zip, Coordinate(x=2737.0, y=301.0, z=0)) # 正极片(4个洞位,2x2布局) - zhengji_zip = ClipMagazine_four("zhengji_zip", 80, 80, 10) + zhengji_zip = MagazineHolder_4("正极弹夹", 80, 80, 10) self.assign_child_resource(zhengji_zip, Coordinate(x=2799.0, y=356.0, z=0)) # 正极壳(4个洞位,2x2布局) - zhengjike_zip = ClipMagazine_four("zhengjike_zip", 80, 80, 10) + zhengjike_zip = MagazineHolder_4("正极壳弹夹", 80, 80, 10) self.assign_child_resource(zhengjike_zip, Coordinate(x=2586.0, y=1143.0, z=0)) # 垫片(2个洞位,1x2布局) - danpian_zip = ClipMagazine_two("danpian_zip", 80, 80, 10) + danpian_zip = MagazineHolder_2("垫片弹夹", 80, 80, 10) self.assign_child_resource(danpian_zip, Coordinate(x=2690.0, y=1141.0, z=0)) # 负极壳(4个洞位,2x2布局) - fujike_zip = ClipMagazine_four("fujike_zip", 80, 80, 10) + fujike_zip = MagazineHolder_4("负极壳弹夹", 80, 80, 10) self.assign_child_resource(fujike_zip, Coordinate(x=2492.0, y=1144.0, z=0)) # 弹片(2个洞位,1x2布局) - tanpian_zip = ClipMagazine_two("tanpian_zip", 80, 80, 10) + tanpian_zip = MagazineHolder_2("弹片弹夹", 80, 80, 10) self.assign_child_resource(tanpian_zip, Coordinate(x=2492.0, y=1139.0, z=0)) # 成品弹夹(6个洞位,3x2布局) - chengpindanjia_zip = ClipMagazine("chengpindanjia_zip", 80, 80, 10) + chengpindanjia_zip = MagazineHolder_6("成品弹夹", 80, 80, 10) self.assign_child_resource(chengpindanjia_zip, Coordinate(x=3112.0, y=1295.0, z=0)) # 为子弹夹添加极片 - for i in range(1): # ClipMagazine_one 有1个洞位 - lvbo = ElectrodeSheet(name=f"lvbo_{i}", size_x=12, size_y=12, size_z=0.1) + for i in range(1): # MagazineHolder_1 有1个洞位 + lvbo = ElectrodeSheet(name=f"铝箔_{i}", size_x=12, size_y=12, size_z=0.1) lvbo_zip.children[i].assign_child_resource(lvbo, location=None) - for i in range(4): # ClipMagazine_four 有4个洞位 - zhengji = ElectrodeSheet(name=f"zhengji_{i}", size_x=12, size_y=12, size_z=0.1) + for i in range(4): # MagazineHolder_4 有4个洞位 + zhengji = ElectrodeSheet(name=f"正极_{i}", size_x=12, size_y=12, size_z=0.1) zhengji_zip.children[i].assign_child_resource(zhengji, location=None) - for i in range(4): # ClipMagazine_four 有4个洞位 - zhengjike = ElectrodeSheet(name=f"zhengjike_{i}", size_x=12, size_y=12, size_z=0.1) + for i in range(4): # MagazineHolder_4 有4个洞位 + zhengjike = ElectrodeSheet(name=f"正极壳_{i}", size_x=12, size_y=12, size_z=0.1) zhengjike_zip.children[i].assign_child_resource(zhengjike, location=None) - for i in range(2): # ClipMagazine_two 有2个洞位 - danpian = ElectrodeSheet(name=f"danpian_{i}", size_x=12, size_y=12, size_z=0.1) + for i in range(2): # MagazineHolder_2 有2个洞位 + danpian = ElectrodeSheet(name=f"垫片_{i}", size_x=12, size_y=12, size_z=0.1) danpian_zip.children[i].assign_child_resource(danpian, location=None) - for i in range(4): # ClipMagazine_four 有4个洞位 - fujike = ElectrodeSheet(name=f"fujike_{i}", size_x=12, size_y=12, size_z=0.1) + for i in range(4): # MagazineHolder_4 有4个洞位 + fujike = ElectrodeSheet(name=f"负极壳_{i}", size_x=12, size_y=12, size_z=0.1) fujike_zip.children[i].assign_child_resource(fujike, location=None) - for i in range(2): # ClipMagazine_two 有2个洞位 - tanpian = ElectrodeSheet(name=f"tanpian_{i}", size_x=12, size_y=12, size_z=0.1) + for i in range(2): # MagazineHolder_2 有2个洞位 + tanpian = ElectrodeSheet(name=f"弹片_{i}", size_x=12, size_y=12, size_z=0.1) tanpian_zip.children[i].assign_child_resource(tanpian, location=None) - for i in range(6): # ClipMagazine 有6个洞位 - chengpindanjia = ElectrodeSheet(name=f"chengpindanjia_{i}", size_x=12, size_y=12, size_z=0.1) - chengpindanjia_zip.children[i].assign_child_resource(chengpindanjia, location=None) + # for i in range(6): # MagazineHolder_6 有6个洞位 + # chengpindanjia = ElectrodeSheet(name=f"成品弹夹_{i}", size_x=12, size_y=12, size_z=0.1) + # chengpindanjia_zip.children[i].assign_child_resource(chengpindanjia, location=None) # ====================================== 物料板 ============================================ # 创建物料板(料盘carrier)- 4x4布局 # 负极料盘 - fujiliaopan = MaterialPlate(name="fujiliaopan", size_x=120, size_y=100, size_z=10.0, fill=True) + fujiliaopan = MaterialPlate(name="负极料盘", size_x=120, size_y=100, size_z=10.0, fill=True) self.assign_child_resource(fujiliaopan, Coordinate(x=2107.0, y=304.0, z=0)) - for i in range(16): - fujipian = ElectrodeSheet(name=f"{fujiliaopan.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - fujiliaopan.children[i].assign_child_resource(fujipian, location=None) + # for i in range(16): + # fujipian = ElectrodeSheet(name=f"{fujiliaopan.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + # fujiliaopan.children[i].assign_child_resource(fujipian, location=None) # 隔膜料盘 - gemoliaopan = MaterialPlate(name="gemoliaopan", size_x=120, size_y=100, size_z=10.0, fill=True) + gemoliaopan = MaterialPlate(name="隔膜料盘", size_x=120, size_y=100, size_z=10.0, fill=True) self.assign_child_resource(gemoliaopan, Coordinate(x=2107.0, y=146.0, z=0)) - for i in range(16): - gemopian = ElectrodeSheet(name=f"{gemoliaopan.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - gemoliaopan.children[i].assign_child_resource(gemopian, location=None) + # for i in range(16): + # gemopian = ElectrodeSheet(name=f"{gemoliaopan.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1) + # gemoliaopan.children[i].assign_child_resource(gemopian, location=None) # ====================================== 瓶架、移液枪 ============================================ # 在台面上放置 3x4 瓶架、6x2 瓶架 与 64孔移液枪头盒 - # 奔耀上料5ml分液瓶小板 - 2x4布局 - bottle_rack_2x4 = BottleRack( - name="bottle_rack_3x4", - size_x=210.0, - size_y=140.0, - size_z=100.0, - num_items_x=2, - num_items_y=4, - position_spacing=35.0, - orientation="vertical", - ) - self.assign_child_resource(bottle_rack_2x4, Coordinate(x=1542.0, y=717.0, z=0)) + # 奔耀上料5ml分液瓶小板 - 由奔曜跨站转运而来,不单独写 + + # bottle_rack_3x4 = BottleRack( + # name="bottle_rack_3x4", + # size_x=210.0, + # size_y=140.0, + # size_z=100.0, + # num_items_x=2, + # num_items_y=4, + # position_spacing=35.0, + # orientation="vertical", + # ) + # self.assign_child_resource(bottle_rack_3x4, Coordinate(x=1542.0, y=717.0, z=0)) # 电解液缓存位 - 6x2布局 - bottle_rack_6x2 = BottleRack( - name="bottle_rack_6x2", - size_x=120.0, - size_y=250.0, - size_z=100.0, - num_items_x=6, - num_items_y=2, - position_spacing=35.0, - orientation="vertical", - ) + bottle_rack_6x2 = YIHUA_Electrolyte_12VialCarrier(name="bottle_rack_6x2") self.assign_child_resource(bottle_rack_6x2, Coordinate(x=300, y=300, z=0)) # 电解液回收位6x2 - bottle_rack_2x6_2 = BottleRack( - name="bottle_rack_6x2_2", - size_x=120.0, - size_y=250.0, - size_z=100.0, - num_items_x=6, - num_items_y=2, - position_spacing=35.0, - orientation="vertical", - ) + bottle_rack_6x2_2 = YIHUA_Electrolyte_12VialCarrier(name="bottle_rack_6x2_2") self.assign_child_resource(bottle_rack_6x2_2, Coordinate(x=1765.0, y=869.0, z=0)) - # 将 ElectrodeSheet 放满 3x4 与 6x2 的所有孔位 - for idx in range(bottle_rack_2x4.num_items_x * bottle_rack_2x4.num_items_y): - sheet = ElectrodeSheet(name=f"sheet_3x4_{idx}", size_x=12, size_y=12, size_z=0.1) - bottle_rack_2x4.assign_child_resource(sheet, index=idx) - - for idx in range(bottle_rack_6x2.num_items_x * bottle_rack_6x2.num_items_y): - sheet = ElectrodeSheet(name=f"sheet_6x2_{idx}", size_x=12, size_y=12, size_z=0.1) - bottle_rack_6x2.assign_child_resource(sheet, index=idx) - tip_box = TipBox64(name="tip_box_64") self.assign_child_resource(tip_box, Coordinate(x=1938.0, y=743.0, z=0)) @@ -1278,7 +727,6 @@ class CoincellDeck(Deck): self.assign_child_resource(waste_tip_box, Coordinate(x=1960.0, y=639.0, z=0)) - if __name__ == "__main__": deck = CoincellDeck(setup=True) print(deck) \ No newline at end of file diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py index 21682a9e..f7f8e33b 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py @@ -112,19 +112,18 @@ class CoinCellAssemblyWorkstation(WorkstationBase): def __init__(self, config: dict = None, deck=None, - address: str = "172.21.33.176", - port: str = "502", - debug_mode: bool = False, + address: str = "172.21.33.176", + port: str = "502", + debug_mode: bool = False, *args, **kwargs): if deck is None and config: deck = config.get('deck') - else : + if deck is None: logger.info("没有传入依华deck,检查启动json文件") super().__init__(deck=deck, *args, **kwargs,) self.debug_mode = debug_mode - """ 连接初始化 """ modbus_client = TCPClient(addr=address, port=port) @@ -140,12 +139,14 @@ class CoinCellAssemblyWorkstation(WorkstationBase): time.sleep(2) if not modbus_client.client.is_socket_open(): raise ValueError('modbus tcp connection failed') + 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) else: print("测试模式,跳过连接") + self.nodes, self.client = None, None """ 工站的配置 """ - 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 @@ -153,8 +154,6 @@ class CoinCellAssemblyWorkstation(WorkstationBase): self.csv_export_file = None self.coin_num_N = 0 #已组装电池数量 - - def post_init(self, ros_node: ROS2WorkstationNode): self._ros_node = ros_node #self.deck = create_a_coin_cell_deck() diff --git a/unilabos/devices/workstation/workstation_material_management.py b/unilabos/devices/workstation/workstation_material_management.py deleted file mode 100644 index a9229130..00000000 --- a/unilabos/devices/workstation/workstation_material_management.py +++ /dev/null @@ -1,583 +0,0 @@ -""" -工作站物料管理基类 -Workstation Material Management Base Class - -基于PyLabRobot的物料管理系统 -""" -from typing import Dict, Any, List, Optional, Union, Type -from abc import ABC, abstractmethod -import json - -from pylabrobot.resources import ( - Resource as PLRResource, - Container, - Deck, - Coordinate as PLRCoordinate, -) - -from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker -from unilabos.utils.log import logger -from unilabos.resources.graphio import resource_plr_to_ulab, resource_ulab_to_plr - - -class MaterialManagementBase(ABC): - """物料管理基类 - - 定义工作站物料管理的标准接口: - 1. 物料初始化 - 根据配置创建物料资源 - 2. 物料追踪 - 实时跟踪物料位置和状态 - 3. 物料查找 - 按类型、位置、状态查找物料 - 4. 物料转换 - PyLabRobot与UniLab资源格式转换 - """ - - def __init__( - self, - device_id: str, - deck_config: Dict[str, Any], - resource_tracker: DeviceNodeResourceTracker, - children_config: Dict[str, Dict[str, Any]] = None - ): - self.device_id = device_id - self.deck_config = deck_config - self.resource_tracker = resource_tracker - self.children_config = children_config or {} - - # 创建主台面 - self.plr_deck = self._create_deck() - - # 扩展ResourceTracker - self._extend_resource_tracker() - - # 注册deck到resource tracker - self.resource_tracker.add_resource(self.plr_deck) - - # 初始化子资源 - self.plr_resources = {} - self._initialize_materials() - - def _create_deck(self) -> Deck: - """创建主台面""" - return Deck( - name=f"{self.device_id}_deck", - 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", 500.0), - origin=PLRCoordinate(0, 0, 0) - ) - - def _extend_resource_tracker(self): - """扩展ResourceTracker以支持PyLabRobot特定功能""" - - def find_by_type(resource_type): - """按类型查找资源""" - return self._find_resources_by_type_recursive(self.plr_deck, resource_type) - - def find_by_category(category: str): - """按类别查找资源""" - found = [] - for resource in self._get_all_resources(): - if hasattr(resource, 'category') and resource.category == category: - found.append(resource) - return found - - def find_by_name_pattern(pattern: str): - """按名称模式查找资源""" - import re - found = [] - for resource in self._get_all_resources(): - if re.search(pattern, resource.name): - found.append(resource) - return found - - # 动态添加方法到resource_tracker - self.resource_tracker.find_by_type = find_by_type - self.resource_tracker.find_by_category = find_by_category - self.resource_tracker.find_by_name_pattern = find_by_name_pattern - - def _find_resources_by_type_recursive(self, resource, target_type): - """递归查找指定类型的资源""" - found = [] - if isinstance(resource, target_type): - found.append(resource) - - # 递归查找子资源 - children = getattr(resource, "children", []) - for child in children: - found.extend(self._find_resources_by_type_recursive(child, target_type)) - - return found - - def _get_all_resources(self) -> List[PLRResource]: - """获取所有资源""" - all_resources = [] - - def collect_resources(resource): - all_resources.append(resource) - children = getattr(resource, "children", []) - for child in children: - collect_resources(child) - - collect_resources(self.plr_deck) - return all_resources - - def _initialize_materials(self): - """初始化物料""" - try: - # 确定创建顺序,确保父资源先于子资源创建 - creation_order = self._determine_creation_order() - - # 按顺序创建资源 - for resource_id in creation_order: - config = self.children_config[resource_id] - self._create_plr_resource(resource_id, config) - - logger.info(f"物料管理系统初始化完成,共创建 {len(self.plr_resources)} 个资源") - - except Exception as e: - logger.error(f"物料初始化失败: {e}") - - def _determine_creation_order(self) -> List[str]: - """确定资源创建顺序""" - order = [] - visited = set() - - def visit(resource_id: str): - if resource_id in visited: - return - visited.add(resource_id) - - config = self.children_config.get(resource_id, {}) - parent_id = config.get("parent") - - # 如果有父资源,先访问父资源 - if parent_id and parent_id in self.children_config: - visit(parent_id) - - order.append(resource_id) - - for resource_id in self.children_config: - visit(resource_id) - - return order - - def _create_plr_resource(self, resource_id: str, config: Dict[str, Any]): - """创建PyLabRobot资源""" - try: - resource_type = config.get("type", "unknown") - data = config.get("data", {}) - location_config = config.get("location", {}) - - # 创建位置坐标 - location = PLRCoordinate( - x=location_config.get("x", 0.0), - y=location_config.get("y", 0.0), - z=location_config.get("z", 0.0) - ) - - # 根据类型创建资源 - resource = self._create_resource_by_type(resource_id, resource_type, config, data, location) - - if resource: - # 设置父子关系 - parent_id = config.get("parent") - if parent_id and parent_id in self.plr_resources: - parent_resource = self.plr_resources[parent_id] - parent_resource.assign_child_resource(resource, location) - else: - # 直接放在deck上 - self.plr_deck.assign_child_resource(resource, location) - - # 保存资源引用 - self.plr_resources[resource_id] = resource - - # 注册到resource tracker - self.resource_tracker.add_resource(resource) - - logger.debug(f"创建资源成功: {resource_id} ({resource_type})") - - except Exception as e: - logger.error(f"创建资源失败 {resource_id}: {e}") - - @abstractmethod - def _create_resource_by_type( - self, - resource_id: str, - resource_type: str, - config: Dict[str, Any], - data: Dict[str, Any], - location: PLRCoordinate - ) -> Optional[PLRResource]: - """根据类型创建资源 - 子类必须实现""" - pass - - # ============ 物料查找接口 ============ - - def find_materials_by_type(self, material_type: str) -> List[PLRResource]: - """按材料类型查找物料""" - return self.resource_tracker.find_by_category(material_type) - - def find_material_by_id(self, resource_id: str) -> Optional[PLRResource]: - """按ID查找物料""" - return self.plr_resources.get(resource_id) - - def find_available_positions(self, position_type: str) -> List[PLRResource]: - """查找可用位置""" - positions = self.resource_tracker.find_by_category(position_type) - available = [] - - for pos in positions: - if hasattr(pos, 'is_available') and pos.is_available(): - available.append(pos) - elif hasattr(pos, 'children') and len(pos.children) == 0: - available.append(pos) - - return available - - def get_material_inventory(self) -> Dict[str, int]: - """获取物料库存统计""" - inventory = {} - - for resource in self._get_all_resources(): - if hasattr(resource, 'category'): - category = resource.category - inventory[category] = inventory.get(category, 0) + 1 - - return inventory - - # ============ 物料状态更新接口 ============ - - def update_material_location(self, material_id: str, new_location: PLRCoordinate) -> bool: - """更新物料位置""" - try: - material = self.find_material_by_id(material_id) - if material: - material.location = new_location - return True - return False - except Exception as e: - logger.error(f"更新物料位置失败: {e}") - return False - - def move_material(self, material_id: str, target_container_id: str) -> bool: - """移动物料到目标容器""" - try: - material = self.find_material_by_id(material_id) - target = self.find_material_by_id(target_container_id) - - if material and target: - # 从原位置移除 - if material.parent: - material.parent.unassign_child_resource(material) - - # 添加到新位置 - target.assign_child_resource(material) - return True - - return False - - except Exception as e: - logger.error(f"移动物料失败: {e}") - return False - - # ============ 资源转换接口 ============ - - def convert_to_unilab_format(self, plr_resource: PLRResource) -> Dict[str, Any]: - """将PyLabRobot资源转换为UniLab格式""" - return resource_plr_to_ulab(plr_resource) - - def convert_from_unilab_format(self, unilab_resource: Dict[str, Any]) -> PLRResource: - """将UniLab格式转换为PyLabRobot资源""" - return resource_ulab_to_plr(unilab_resource) - - def get_deck_state(self) -> Dict[str, Any]: - """获取Deck状态""" - try: - return { - "deck_info": { - "name": self.plr_deck.name, - "size": { - "x": self.plr_deck.size_x, - "y": self.plr_deck.size_y, - "z": self.plr_deck.size_z - }, - "children_count": len(self.plr_deck.children) - }, - "resources": { - resource_id: self.convert_to_unilab_format(resource) - for resource_id, resource in self.plr_resources.items() - }, - "inventory": self.get_material_inventory() - } - except Exception as e: - logger.error(f"获取Deck状态失败: {e}") - return {"error": str(e)} - - # ============ 数据持久化接口 ============ - - def save_state_to_file(self, file_path: str) -> bool: - """保存状态到文件""" - try: - state = self.get_deck_state() - with open(file_path, 'w', encoding='utf-8') as f: - json.dump(state, f, indent=2, ensure_ascii=False) - logger.info(f"状态已保存到: {file_path}") - return True - except Exception as e: - logger.error(f"保存状态失败: {e}") - return False - - def load_state_from_file(self, file_path: str) -> bool: - """从文件加载状态""" - try: - with open(file_path, 'r', encoding='utf-8') as f: - state = json.load(f) - - # 重新创建资源 - self._recreate_resources_from_state(state) - logger.info(f"状态已从文件加载: {file_path}") - return True - - except Exception as e: - logger.error(f"加载状态失败: {e}") - return False - - def _recreate_resources_from_state(self, state: Dict[str, Any]): - """从状态重新创建资源""" - # 清除现有资源 - self.plr_resources.clear() - self.plr_deck.children.clear() - - # 从状态重新创建 - resources_data = state.get("resources", {}) - for resource_id, resource_data in resources_data.items(): - try: - plr_resource = self.convert_from_unilab_format(resource_data) - self.plr_resources[resource_id] = plr_resource - self.plr_deck.assign_child_resource(plr_resource) - except Exception as e: - logger.error(f"重新创建资源失败 {resource_id}: {e}") - - -class CoinCellMaterialManagement(MaterialManagementBase): - """纽扣电池物料管理类 - - 从 button_battery_station 抽取的物料管理功能 - """ - - def _create_resource_by_type( - self, - resource_id: str, - resource_type: str, - config: Dict[str, Any], - data: Dict[str, Any], - location: PLRCoordinate - ) -> Optional[PLRResource]: - """根据类型创建纽扣电池相关资源""" - - # 导入纽扣电池资源类 - from unilabos.device_comms.button_battery_station import ( - MaterialPlate, PlateSlot, ClipMagazine, BatteryPressSlot, - TipBox64, WasteTipBox, BottleRack, Battery, ElectrodeSheet - ) - - try: - if resource_type == "material_plate": - return self._create_material_plate(resource_id, config, data, location) - - elif resource_type == "plate_slot": - return self._create_plate_slot(resource_id, config, data, location) - - elif resource_type == "clip_magazine": - return self._create_clip_magazine(resource_id, config, data, location) - - elif resource_type == "battery_press_slot": - return self._create_battery_press_slot(resource_id, config, data, location) - - elif resource_type == "tip_box": - return self._create_tip_box(resource_id, config, data, location) - - elif resource_type == "waste_tip_box": - return self._create_waste_tip_box(resource_id, config, data, location) - - elif resource_type == "bottle_rack": - return self._create_bottle_rack(resource_id, config, data, location) - - elif resource_type == "battery": - return self._create_battery(resource_id, config, data, location) - - else: - logger.warning(f"未知的资源类型: {resource_type}") - return None - - except Exception as e: - logger.error(f"创建资源失败 {resource_id} ({resource_type}): {e}") - return None - - def _create_material_plate(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate): - """创建料板""" - from unilabos.device_comms.button_battery_station import MaterialPlate, ElectrodeSheet - - plate = MaterialPlate( - name=resource_id, - size_x=config.get("size_x", 80.0), - size_y=config.get("size_y", 80.0), - size_z=config.get("size_z", 10.0), - hole_diameter=config.get("hole_diameter", 15.0), - hole_depth=config.get("hole_depth", 8.0), - hole_spacing_x=config.get("hole_spacing_x", 20.0), - hole_spacing_y=config.get("hole_spacing_y", 20.0), - number=data.get("number", "") - ) - plate.location = location - - # 如果有预填充的极片数据,创建极片 - electrode_sheets = data.get("electrode_sheets", []) - for i, sheet_data in enumerate(electrode_sheets): - if i < len(plate.children): # 确保不超过洞位数量 - hole = plate.children[i] - sheet = ElectrodeSheet( - name=f"{resource_id}_sheet_{i}", - diameter=sheet_data.get("diameter", 14.0), - thickness=sheet_data.get("thickness", 0.1), - mass=sheet_data.get("mass", 0.01), - material_type=sheet_data.get("material_type", "cathode"), - info=sheet_data.get("info", "") - ) - hole.place_electrode_sheet(sheet) - - return plate - - def _create_plate_slot(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate): - """创建板槽位""" - from unilabos.device_comms.button_battery_station import PlateSlot - - slot = PlateSlot( - name=resource_id, - max_plates=config.get("max_plates", 8) - ) - slot.location = location - return slot - - def _create_clip_magazine(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate): - """创建子弹夹""" - from unilabos.device_comms.button_battery_station import ClipMagazine - - magazine = ClipMagazine( - name=resource_id, - size_x=config.get("size_x", 150.0), - size_y=config.get("size_y", 100.0), - size_z=config.get("size_z", 50.0), - hole_diameter=config.get("hole_diameter", 15.0), - hole_depth=config.get("hole_depth", 40.0), - hole_spacing=config.get("hole_spacing", 25.0), - max_sheets_per_hole=config.get("max_sheets_per_hole", 100) - ) - magazine.location = location - return magazine - - def _create_battery_press_slot(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate): - """创建电池压制槽""" - from unilabos.device_comms.button_battery_station import BatteryPressSlot - - slot = BatteryPressSlot( - name=resource_id, - diameter=config.get("diameter", 20.0), - depth=config.get("depth", 15.0) - ) - slot.location = location - return slot - - def _create_tip_box(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate): - """创建枪头盒""" - from unilabos.device_comms.button_battery_station import TipBox64 - - tip_box = TipBox64( - name=resource_id, - size_x=config.get("size_x", 127.8), - size_y=config.get("size_y", 85.5), - size_z=config.get("size_z", 60.0), - with_tips=data.get("with_tips", True) - ) - tip_box.location = location - return tip_box - - def _create_waste_tip_box(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate): - """创建废枪头盒""" - from unilabos.device_comms.button_battery_station import WasteTipBox - - waste_box = WasteTipBox( - name=resource_id, - size_x=config.get("size_x", 127.8), - size_y=config.get("size_y", 85.5), - size_z=config.get("size_z", 60.0), - max_tips=config.get("max_tips", 100) - ) - waste_box.location = location - return waste_box - - def _create_bottle_rack(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate): - """创建瓶架""" - from unilabos.device_comms.button_battery_station import BottleRack - - rack = BottleRack( - name=resource_id, - size_x=config.get("size_x", 210.0), - size_y=config.get("size_y", 140.0), - size_z=config.get("size_z", 100.0), - bottle_diameter=config.get("bottle_diameter", 30.0), - bottle_height=config.get("bottle_height", 100.0), - position_spacing=config.get("position_spacing", 35.0) - ) - rack.location = location - return rack - - def _create_battery(self, resource_id: str, config: Dict[str, Any], data: Dict[str, Any], location: PLRCoordinate): - """创建电池""" - from unilabos.device_comms.button_battery_station import Battery - - battery = Battery( - name=resource_id, - diameter=config.get("diameter", 20.0), - height=config.get("height", 3.2), - max_volume=config.get("max_volume", 100.0), - barcode=data.get("barcode", "") - ) - battery.location = location - return battery - - # ============ 纽扣电池特定查找方法 ============ - - def find_material_plates(self): - """查找所有料板""" - from unilabos.device_comms.button_battery_station import MaterialPlate - return self.resource_tracker.find_by_type(MaterialPlate) - - def find_batteries(self): - """查找所有电池""" - from unilabos.device_comms.button_battery_station import Battery - return self.resource_tracker.find_by_type(Battery) - - def find_electrode_sheets(self): - """查找所有极片""" - found = [] - plates = self.find_material_plates() - for plate in plates: - for hole in plate.children: - if hasattr(hole, 'has_electrode_sheet') and hole.has_electrode_sheet(): - found.append(hole._electrode_sheet) - return found - - def find_plate_slots(self): - """查找所有板槽位""" - from unilabos.device_comms.button_battery_station import PlateSlot - return self.resource_tracker.find_by_type(PlateSlot) - - def find_clip_magazines(self): - """查找所有子弹夹""" - from unilabos.device_comms.button_battery_station import ClipMagazine - return self.resource_tracker.find_by_type(ClipMagazine) - - def find_press_slots(self): - """查找所有压制槽""" - from unilabos.device_comms.button_battery_station import BatteryPressSlot - return self.resource_tracker.find_by_type(BatteryPressSlot) diff --git a/unilabos/registry/resources/bioyond/deck.yaml b/unilabos/registry/resources/bioyond/deck.yaml index d28218b4..07f78ea4 100644 --- a/unilabos/registry/resources/bioyond/deck.yaml +++ b/unilabos/registry/resources/bioyond/deck.yaml @@ -46,6 +46,3 @@ CoincellDeck: init_param_schema: {} registry_type: resource version: 1.0.0 - - - diff --git a/unilabos/resources/battery/bottle_carriers.py b/unilabos/resources/battery/bottle_carriers.py new file mode 100644 index 00000000..9d9827cd --- /dev/null +++ b/unilabos/resources/battery/bottle_carriers.py @@ -0,0 +1,56 @@ +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.YB_bottles import ( + YB_pei_ye_xiao_Bottle, +) +# 命名约定:试剂瓶-Bottle,烧杯-Beaker,烧瓶-Flask,小瓶-Vial + + +def YIHUA_Electrolyte_12VialCarrier(name: str) -> BottleCarrier: + """12瓶载架 - 2x6布局""" + # 载架尺寸 (mm) + carrier_size_x = 120.0 + carrier_size_y = 250.0 + carrier_size_z = 50.0 + + # 瓶位尺寸 + bottle_diameter = 35.0 + bottle_spacing_x = 35.0 # X方向间距 + bottle_spacing_y = 35.0 # Y方向间距 + + # 计算起始位置 (居中排列) + start_x = (carrier_size_x - (2 - 1) * bottle_spacing_x - bottle_diameter) / 2 + start_y = (carrier_size_y - (6 - 1) * bottle_spacing_y - bottle_diameter) / 2 + + sites = create_ordered_items_2d( + klass=ResourceHolder, + num_items_x=2, + num_items_y=6, + 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_12VialCarrier", + ) + carrier.num_items_x = 2 + carrier.num_items_y = 6 + carrier.num_items_z = 1 + for i in range(12): + carrier[i] = YB_pei_ye_xiao_Bottle(f"{name}_vial_{i+1}") + return carrier diff --git a/unilabos/resources/battery/magazine.py b/unilabos/resources/battery/magazine.py new file mode 100644 index 00000000..a5a15ccc --- /dev/null +++ b/unilabos/resources/battery/magazine.py @@ -0,0 +1,284 @@ +from typing import Dict, List, Optional, OrderedDict, Union +import math + +from pylabrobot.resources.coordinate import Coordinate +from pylabrobot.resources import Resource, ResourceStack, ItemizedResource +from pylabrobot.resources.carrier import create_homogeneous_resources + + +class Magazine(ResourceStack): + """子弹夹洞位类""" + + def __init__( + self, + name: str, + direction: str = 'z', + resources: Optional[List[Resource]] = None, + max_sheets: int = 100, + **kwargs + ): + """初始化子弹夹洞位 + + Args: + name: 洞位名称 + direction: 堆叠方向 + resources: 资源列表 + max_sheets: 最大极片数量 + """ + super().__init__( + name=name, + direction=direction, + resources=resources, + ) + self.max_sheets = max_sheets + + +class MagazineHolder(ItemizedResource): + """子弹夹类 - 有多个洞位,每个洞位放多个极片""" + + def __init__( + self, + name: str, + size_x: float, + size_y: float, + size_z: float, + ordered_items: Optional[Dict[str, Magazine]] = None, + ordering: Optional[OrderedDict[str, str]] = None, + hole_diameter: float = 14.0, + hole_depth: float = 10.0, + max_sheets_per_hole: int = 100, + cross_section_type: str = "circle", + category: str = "magazine_holder", + model: Optional[str] = None, + ): + """初始化子弹夹 + + Args: + name: 子弹夹名称 + size_x: 长度 (mm) + size_y: 宽度 (mm) + size_z: 高度 (mm) + hole_diameter: 洞直径 (mm) + hole_depth: 洞深度 (mm) + max_sheets_per_hole: 每个洞位最大极片数量 + category: 类别 + model: 型号 + """ + + 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, + ) + + # 保存洞位的直径和深度 + self.hole_diameter = hole_diameter + self.hole_depth = hole_depth + self.max_sheets_per_hole = max_sheets_per_hole + self.cross_section_type = cross_section_type + + 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, + "cross_section_type": self.cross_section_type, + } + + +def magazine_factory( + name: str, + size_x: float, + size_y: float, + size_z: float, + locations: List[Coordinate], + hole_diameter: float = 14.0, + hole_depth: float = 10.0, + max_sheets_per_hole: int = 100, + category: str = "magazine_holder", + model: Optional[str] = None, +) -> 'MagazineHolder': + """工厂函数:创建子弹夹 + + Args: + name: 子弹夹名称 + size_x: 长度 (mm) + size_y: 宽度 (mm) + size_z: 高度 (mm) + locations: 洞位坐标列表 + hole_diameter: 洞直径 (mm) + hole_depth: 洞深度 (mm) + max_sheets_per_hole: 每个洞位最大极片数量 + category: 类别 + model: 型号 + """ + # 创建洞位 + _sites = create_homogeneous_resources( + klass=Magazine, + locations=locations, + resource_size_x=hole_diameter, + resource_size_y=hole_diameter, + name_prefix=name, + max_sheets=max_sheets_per_hole, + ) + + # 生成编号键 + keys = [f"A{i+1}" for i in range(len(locations))] + sites = dict(zip(keys, _sites.values())) + + return MagazineHolder( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + ordered_items=sites, + hole_diameter=hole_diameter, + hole_depth=hole_depth, + max_sheets_per_hole=max_sheets_per_hole, + category=category, + model=model, + ) + + +def MagazineHolder_4( + name: str, + size_x: float = 80.0, + size_y: float = 80.0, + size_z: float = 10.0, + hole_diameter: float = 14.0, + hole_depth: float = 10.0, + hole_spacing: float = 25.0, + max_sheets_per_hole: int = 100, +) -> MagazineHolder: + """创建4孔子弹夹 - 正方形四角排布""" + # 计算4个洞位的坐标(正方形四角排布) + center_x = size_x / 2 + center_y = size_y / 2 + offset = hole_spacing / 2 + + locations = [ + Coordinate(center_x - offset, center_y - offset, size_z - hole_depth), # 左下 + Coordinate(center_x + offset, center_y - offset, size_z - hole_depth), # 右下 + Coordinate(center_x - offset, center_y + offset, size_z - hole_depth), # 左上 + Coordinate(center_x + offset, center_y + offset, size_z - hole_depth), # 右上 + ] + + return magazine_factory( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + locations=locations, + hole_diameter=hole_diameter, + hole_depth=hole_depth, + max_sheets_per_hole=max_sheets_per_hole, + category="clip_magazine_four", + ) + + +def MagazineHolder_2( + name: str, + size_x: float = 80.0, + size_y: float = 80.0, + size_z: float = 10.0, + hole_diameter: float = 14.0, + hole_depth: float = 10.0, + hole_spacing: float = 25.0, + max_sheets_per_hole: int = 100, +) -> MagazineHolder: + """创建2孔子弹夹 - 竖向排布""" + # 计算2个洞位的坐标(竖向排布) + center_x = size_x / 2 + center_y = size_y / 2 + offset = hole_spacing / 2 + + locations = [ + Coordinate(center_x, center_y - offset, size_z - hole_depth), # 下方 + Coordinate(center_x, center_y + offset, size_z - hole_depth), # 上方 + ] + + return magazine_factory( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + locations=locations, + hole_diameter=hole_diameter, + hole_depth=hole_depth, + max_sheets_per_hole=max_sheets_per_hole, + category="clip_magazine_two", + ) + + +def MagazineHolder_1( + name: str, + size_x: float = 80.0, + size_y: float = 80.0, + size_z: float = 10.0, + hole_diameter: float = 14.0, + hole_depth: float = 10.0, + max_sheets_per_hole: int = 100, +) -> MagazineHolder: + """创建1孔子弹夹 - 中心单孔""" + # 计算1个洞位的坐标(中心位置) + center_x = size_x / 2 + center_y = size_y / 2 + + locations = [ + Coordinate(center_x, center_y, size_z - hole_depth), # 中心 + ] + + return magazine_factory( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + locations=locations, + hole_diameter=hole_diameter, + hole_depth=hole_depth, + max_sheets_per_hole=max_sheets_per_hole, + category="clip_magazine_one", + ) + + +def MagazineHolder_6( + name: str, + size_x: float = 80.0, + size_y: float = 80.0, + size_z: float = 40.0, + hole_diameter: float = 14.0, + hole_depth: float = 10.0, + hole_spacing: float = 20.0, + max_sheets_per_hole: int = 100, +) -> MagazineHolder: + """创建6孔子弹夹 - 六边形排布""" + # 计算6个洞位的坐标(六边形排布:中心1个,周围5个) + center_x = size_x / 2 + center_y = size_y / 2 + + locations = [] + + # 周围6个孔,按六边形排布 + for i in range(6): + angle = i * 60 * math.pi / 180 # 每60度一个孔 + x = center_x + hole_spacing * math.cos(angle) + y = center_y + hole_spacing * math.sin(angle) + locations.append(Coordinate(x, y, size_z - hole_depth)) + + return magazine_factory( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + locations=locations, + hole_diameter=hole_diameter, + hole_depth=hole_depth, + max_sheets_per_hole=max_sheets_per_hole, + category="clip_magazine_six", + ) \ No newline at end of file diff --git a/unilabos/resources/itemized_carrier.py b/unilabos/resources/itemized_carrier.py index fef09e25..831a0734 100644 --- a/unilabos/resources/itemized_carrier.py +++ b/unilabos/resources/itemized_carrier.py @@ -29,7 +29,7 @@ class Bottle(Well): size_x: float = 0.0, size_y: float = 0.0, size_z: float = 0.0, - barcode: Optional[str] = "", + barcode: Optional[str] = None, category: str = "container", model: Optional[str] = None, **kwargs,