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/new_cellconfig3c.json b/new_cellconfig3c.json index 446d2357..2a5d6ebc 100644 --- a/new_cellconfig3c.json +++ b/new_cellconfig3c.json @@ -47,21 +47,50 @@ { "id": "BatteryStation", "name": "扣电工作站", + "parent": null, "children": [ "coin_cell_deck" ], - "parent": null, "type": "device", - "class": "coincellassemblyworkstation_device", + "class":"coincellassemblyworkstation_device", "position": { - "x": 600, - "y": 400, + "x": -600, + "y": -400, "z": 0 }, "config": { - "debug_mode": false, + "deck": { + "data": { + "_resource_child_name": "YB_YH_Deck", + "_resource_type": "unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:CoincellDeck" + } + }, "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": [] 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 48780a57..5ca2bb0f 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 @@ -17,6 +17,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 @@ -1156,17 +1157,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 a2dff68a..1e929c80 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py +++ b/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py @@ -18,67 +18,11 @@ 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_4_Cathode, MagazineHolder_6_Cathode, MagazineHolder_6_Anode, MagazineHolder_6_Battery +from unilabos.resources.battery.bottle_carriers import YIHUA_Electrolyte_12VialCarrier -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): @@ -165,7 +109,6 @@ class MaterialHole(Resource): return self.children[index] - class MaterialPlateState(TypedDict): hole_spacing_x: float hole_spacing_y: float @@ -327,132 +270,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 +412,56 @@ 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", + category: str = "tip_rack", 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, + # 创建12x8=96个枪头位 + 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} + tip_rack = TipRack( + name=name, + size_x=size_x, + size_y=size_y, + size_z=size_z, + # ordered_items=tip_spots_available, + ordered_items=tip_spots, + category=category, + model=model, + with_tips=False, + ) + tip_rack.set_tip_state([True]*32 + [False]*32 + [True]*32) # 前32和后32个有枪头,中间32个无枪头 + return tip_rack class WasteTipBoxstate(TypedDict): @@ -682,8 +479,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,253 +534,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 = 3, - num_items_y: int = 4, - position_spacing: float = 35.0, - orientation: str = "horizontal", - padding_x: float = 20.0, - padding_y: float = 20.0, - ): - """初始化瓶架 - - Args: - name: 瓶架名称 - size_x: 长度 (mm) - size_y: 宽度 (mm) - size_z: 高度 (mm) - category: 类别 - model: 型号 - """ - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - category=category, - model=model, - ) - # 初始化状态 - self._unilabos_state: BottleRackState = BottleRackState( - bottle_diameter=30.0, - bottle_height=100.0, - position_spacing=position_spacing, - name_to_index={}, - ) - # 基于网格生成瓶位坐标映射(居中摆放) - # 使用内边距,避免点跑到容器外(前端渲染不按mm等比缩放时更稳妥) - origin_x = padding_x - origin_y = padding_y - self.index_to_pos = {} - for j in range(num_items_y): - for i in range(num_items_x): - idx = j * num_items_x + i - if orientation == "vertical": - # 纵向:沿 y 方向优先排列 - self.index_to_pos[idx] = Coordinate( - x=origin_x + j * position_spacing, - y=origin_y + i * position_spacing, - z=0, - ) - else: - # 横向(默认):沿 x 方向优先排列 - self.index_to_pos[idx] = Coordinate( - x=origin_x + i * position_spacing, - y=origin_y + j * position_spacing, - z=0, - ) - self.name_to_index = {} - self.name_to_pos = {} - self.num_items_x = num_items_x - self.num_items_y = num_items_y - self.orientation = orientation - self.padding_x = padding_x - self.padding_y = padding_y - - def load_state(self, state: Dict[str, Any]) -> None: - """格式不变""" - super().load_state(state) - self._unilabos_state = state - - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - """格式不变""" - data = super().serialize_state() - data.update( - self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - - # TODO: 这里有些问题要重新写一下 - def assign_child_resource_old(self, resource: Resource, location=Coordinate.zero(), reassign=True): - capacity = self.num_items_x * self.num_items_y - assert len(self.children) < capacity, "瓶架已满,无法添加更多瓶子" - index = len(self.children) - location = self.index_to_pos.get(index, Coordinate.zero()) - self.name_to_pos[resource.name] = location - self.name_to_index[resource.name] = index - return super().assign_child_resource(resource, location, reassign) - - def assign_child_resource(self, resource: Resource, index: int): - capacity = self.num_items_x * self.num_items_y - assert 0 <= index < capacity, "无效的瓶子索引" - self.name_to_index[resource.name] = index - location = self.index_to_pos[index] - return super().assign_child_resource(resource, location) - - def unassign_child_resource(self, resource: Bottle): - super().unassign_child_resource(resource) - self.index_to_pos.pop(self.name_to_index.pop(resource.name, None), None) - - def serialize(self) -> dict: - return { - **super().serialize(), - "num_items_x": self.num_items_x, - "num_items_y": self.num_items_y, - "position_spacing": self._unilabos_state.get("position_spacing", 35.0), - "orientation": self.orientation, - "padding_x": self.padding_x, - "padding_y": self.padding_y, - } - -class BottleState(TypedDict): - diameter: float - height: float - electrolyte_name: str - electrolyte_volume: float - max_volume: float - -class Bottle(Resource): - """瓶子类 - 容纳电解液""" - - def __init__( - self, - name: str, - category: str = "bottle", - ): - """初始化瓶子 - - Args: - name: 瓶子名称 - diameter: 直径 (mm) - height: 高度 (mm) - max_volume: 最大体积 (μL) - barcode: 二维码 - category: 类别 - model: 型号 - """ - super().__init__( - name=name, - size_x=1, - size_y=1, - size_z=1, - category=category, - ) - self._unilabos_state: BottleState = BottleState() - - def aspirate_electrolyte(self, volume: float) -> bool: - current_volume = self._unilabos_state["electrolyte_volume"] - assert current_volume > volume, f"Cannot aspirate {volume}μL, only {current_volume}μL available." - self._unilabos_state["electrolyte_volume"] -= volume - return True - - def load_state(self, state: Dict[str, Any]) -> None: - """格式不变""" - super().load_state(state) - self._unilabos_state = state - - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - """格式不变""" - data = super().serialize_state() - data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - -class 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 CoincellDeck(Deck): """纽扣电池组装工作站台面类""" @@ -1018,146 +572,67 @@ class CoincellDeck(Deck): def setup(self) -> None: """设置工作站的标准布局 - 包含子弹夹、料盘、瓶架等完整配置""" # ====================================== 子弹夹 ============================================ - zip_dan_jia = ClipMagazine_four("zi_dan_jia", 80, 80, 10) - self.assign_child_resource(zip_dan_jia, Coordinate(x=1400, y=50, z=0)) - zip_dan_jia2 = ClipMagazine_four("zi_dan_jia2", 80, 80, 10) - self.assign_child_resource(zip_dan_jia2, Coordinate(x=1600, y=200, z=0)) - zip_dan_jia3 = ClipMagazine("zi_dan_jia3", 80, 80, 10) - self.assign_child_resource(zip_dan_jia3, Coordinate(x=1500, y=200, z=0)) - zip_dan_jia4 = ClipMagazine("zi_dan_jia4", 80, 80, 10) - self.assign_child_resource(zip_dan_jia4, Coordinate(x=1500, y=300, z=0)) - zip_dan_jia5 = ClipMagazine("zi_dan_jia5", 80, 80, 10) - self.assign_child_resource(zip_dan_jia5, Coordinate(x=1600, y=300, z=0)) - zip_dan_jia6 = ClipMagazine("zi_dan_jia6", 80, 80, 10) - self.assign_child_resource(zip_dan_jia6, Coordinate(x=1530, y=500, z=0)) - zip_dan_jia7 = ClipMagazine("zi_dan_jia7", 80, 80, 10) - self.assign_child_resource(zip_dan_jia7, Coordinate(x=1180, y=400, z=0)) - zip_dan_jia8 = ClipMagazine("zi_dan_jia8", 80, 80, 10) - self.assign_child_resource(zip_dan_jia8, Coordinate(x=1280, y=400, z=0)) - # 为子弹夹添加极片 - for i in range(4): - jipian = ElectrodeSheet(name=f"zi_dan_jia_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - zip_dan_jia2.children[i].assign_child_resource(jipian, location=None) - for i in range(4): - jipian2 = ElectrodeSheet(name=f"zi_dan_jia2_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - zip_dan_jia.children[i].assign_child_resource(jipian2, location=None) - for i in range(6): - jipian3 = ElectrodeSheet(name=f"zi_dan_jia3_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - zip_dan_jia3.children[i].assign_child_resource(jipian3, location=None) - for i in range(6): - jipian4 = ElectrodeSheet(name=f"zi_dan_jia4_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - zip_dan_jia4.children[i].assign_child_resource(jipian4, location=None) - for i in range(6): - jipian5 = ElectrodeSheet(name=f"zi_dan_jia5_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - zip_dan_jia5.children[i].assign_child_resource(jipian5, location=None) - for i in range(6): - jipian6 = ElectrodeSheet(name=f"zi_dan_jia6_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - zip_dan_jia6.children[i].assign_child_resource(jipian6, location=None) - for i in range(6): - jipian7 = ElectrodeSheet(name=f"zi_dan_jia7_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - zip_dan_jia7.children[i].assign_child_resource(jipian7, location=None) - for i in range(6): - jipian8 = ElectrodeSheet(name=f"zi_dan_jia8_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - zip_dan_jia8.children[i].assign_child_resource(jipian8, location=None) + # 正极片(4个洞位,2x2布局) + zhengji_zip = MagazineHolder_4_Cathode("正极&铝箔弹夹") + self.assign_child_resource(zhengji_zip, Coordinate(x=2799.0, y=356.0, z=0)) + + # 正极壳、平垫片(6个洞位,2x2+2布局) + zhengjike_zip = MagazineHolder_6_Cathode("正极壳&平垫片弹夹") + self.assign_child_resource(zhengjike_zip, Coordinate(x=2586.0, y=1143.0, z=0)) + + # 负极壳、弹垫片(6个洞位,2x2+2布局) + fujike_zip = MagazineHolder_6_Anode("负极壳&弹垫片弹夹") + self.assign_child_resource(fujike_zip, Coordinate(x=2492.0, y=1144.0, z=0)) + + # 成品弹夹(6个洞位,3x2布局) + chengpindanjia_zip = MagazineHolder_6_Battery("成品弹夹") + self.assign_child_resource(chengpindanjia_zip, Coordinate(x=3112.0, y=1295.0, z=0)) # ====================================== 物料板 ============================================ - # 创建6个4*4的物料板 - liaopan1 = MaterialPlate(name="liaopan1", size_x=120, size_y=100, size_z=10.0, fill=True) - self.assign_child_resource(liaopan1, Coordinate(x=1010, y=50, z=0)) - for i in range(16): - jipian_1 = ElectrodeSheet(name=f"{liaopan1.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - liaopan1.children[i].assign_child_resource(jipian_1, location=None) + # 创建物料板(料盘carrier)- 4x4布局 + # 负极料盘 + 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) - liaopan2 = MaterialPlate(name="liaopan2", size_x=120, size_y=100, size_z=10.0, fill=True) - self.assign_child_resource(liaopan2, Coordinate(x=1130, y=50, z=0)) + # 隔膜料盘 + 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) - liaopan3 = MaterialPlate(name="liaopan3", size_x=120, size_y=100, size_z=10.0, fill=True) - self.assign_child_resource(liaopan3, Coordinate(x=1250, y=50, z=0)) - - liaopan4 = MaterialPlate(name="liaopan4", size_x=120, size_y=100, size_z=10.0, fill=True) - self.assign_child_resource(liaopan4, Coordinate(x=1010, y=150, z=0)) - for i in range(16): - jipian_4 = ElectrodeSheet(name=f"{liaopan4.name}_jipian_{i}", size_x=12, size_y=12, size_z=0.1) - liaopan4.children[i].assign_child_resource(jipian_4, location=None) - - liaopan5 = MaterialPlate(name="liaopan5", size_x=120, size_y=100, size_z=10.0, fill=True) - self.assign_child_resource(liaopan5, Coordinate(x=1130, y=150, z=0)) - - liaopan6 = MaterialPlate(name="liaopan6", size_x=120, size_y=100, size_z=10.0, fill=True) - self.assign_child_resource(liaopan6, Coordinate(x=1250, y=150, z=0)) - # ====================================== 瓶架、移液枪 ============================================ # 在台面上放置 3x4 瓶架、6x2 瓶架 与 64孔移液枪头盒 - bottle_rack_3x4 = BottleRack( - name="bottle_rack_3x4", - size_x=210.0, - size_y=140.0, - size_z=100.0, - num_items_x=3, - num_items_y=4, - position_spacing=35.0, - orientation="vertical", - ) - self.assign_child_resource(bottle_rack_3x4, Coordinate(x=100, y=200, z=0)) - - bottle_rack_6x2 = BottleRack( - name="bottle_rack_6x2", - size_x=120.0, - size_y=250.0, - size_z=100.0, - num_items_x=6, - num_items_y=2, - position_spacing=35.0, - orientation="vertical", - ) + # 奔耀上料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 = YIHUA_Electrolyte_12VialCarrier(name="bottle_rack_6x2") self.assign_child_resource(bottle_rack_6x2, Coordinate(x=300, y=300, z=0)) - - bottle_rack_6x2_2 = BottleRack( - name="bottle_rack_6x2_2", - size_x=120.0, - size_y=250.0, - size_z=100.0, - num_items_x=6, - num_items_y=2, - position_spacing=35.0, - orientation="vertical", - ) - self.assign_child_resource(bottle_rack_6x2_2, Coordinate(x=430, y=300, z=0)) - - # 将 ElectrodeSheet 放满 3x4 与 6x2 的所有孔位 - for idx in range(bottle_rack_3x4.num_items_x * bottle_rack_3x4.num_items_y): - sheet = ElectrodeSheet(name=f"sheet_3x4_{idx}", size_x=12, size_y=12, size_z=0.1) - bottle_rack_3x4.assign_child_resource(sheet, index=idx) - - for idx in range(bottle_rack_6x2.num_items_x * bottle_rack_6x2.num_items_y): - sheet = ElectrodeSheet(name=f"sheet_6x2_{idx}", size_x=12, size_y=12, size_z=0.1) - bottle_rack_6x2.assign_child_resource(sheet, index=idx) + # 电解液回收位6x2 + 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)) tip_box = TipBox64(name="tip_box_64") - self.assign_child_resource(tip_box, Coordinate(x=300, y=100, z=0)) + self.assign_child_resource(tip_box, Coordinate(x=1938.0, y=743.0, z=0)) waste_tip_box = WasteTipBox(name="waste_tip_box") - self.assign_child_resource(waste_tip_box, Coordinate(x=300, y=200, z=0)) - - print(self) - - -def create_coin_cell_deck(name: str = "coin_cell_deck", size_x: float = 1000.0, size_y: float = 1000.0, size_z: float = 900.0) -> CoincellDeck: - """创建并配置标准的纽扣电池组装工作站台面 - - Args: - name: 台面名称 - size_x: 长度 (mm) - size_y: 宽度 (mm) - size_z: 高度 (mm) - - Returns: - 已配置好的 CoincellDeck 对象 - """ - # 创建 CoincellDeck 实例并自动执行 setup 配置 - deck = CoincellDeck(name=name, size_x=size_x, size_y=size_y, size_z=size_z, setup=True) - return deck + self.assign_child_resource(waste_tip_box, Coordinate(x=1960.0, y=639.0, z=0)) if __name__ == "__main__": 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 b8acb874..8c2cf5f1 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly.py @@ -109,44 +109,22 @@ def _coerce_deck_input(deck: Any) -> Optional[Deck]: #构建物料系统 class CoinCellAssemblyWorkstation(WorkstationBase): - def __init__( - self, - deck: Deck=None, - address: str = "172.16.28.102", + def __init__(self, + config: dict = None, + deck=None, + address: str = "172.21.33.176", port: str = "502", debug_mode: bool = False, *args, - **kwargs, - ): - if deck is None and "deck" in kwargs: - deck = kwargs.pop("deck") - else: - kwargs.pop("deck", None) + **kwargs): - normalized_deck = _coerce_deck_input(deck) - - if deck is None and isinstance(normalized_deck, Deck): - deck = normalized_deck - - super().__init__( - #桌子 - deck=deck, - *args, - **kwargs, - ) + if deck is None and config: + deck = config.get('deck') + if deck is None: + logger.info("没有传入依华deck,检查启动json文件") + super().__init__(deck=deck, *args, **kwargs,) self.debug_mode = debug_mode - - # 如果没有传入 deck,则创建标准配置的 deck - if self.deck is None: - self.deck = CoincellDeck(size_x=1000, size_y=1000, size_z=900, origin=Coordinate(-800, 0, 0),setup=True) - else: - # 如果传入了 deck 但还没有 setup,可以选择是否 setup - if self.deck is not None and len(self.deck.children) == 0: - # deck 为空,执行 setup - self.deck.setup() - # 否则使用传入的 deck(可能已经配置好了) - self.deck = self.deck - + """ 连接初始化 """ modbus_client = TCPClient(addr=address, port=port) logger.debug(f"创建 Modbus 客户端: {modbus_client}") @@ -161,26 +139,21 @@ 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_1105.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._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() 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 664bab3e..07f78ea4 100644 --- a/unilabos/registry/resources/bioyond/deck.yaml +++ b/unilabos/registry/resources/bioyond/deck.yaml @@ -34,3 +34,15 @@ BIOYOND_YB_Deck: init_param_schema: {} registry_type: resource version: 1.0.0 +CoincellDeck: + category: + - deck + class: + module: unilabos.devices.workstation.coin_cell_assembly.YB_YH_materials:CoincellDeck + type: pylabrobot + description: CoincellDeck + handles: [] + icon: yihua.webp + 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/electrode_sheet.py b/unilabos/resources/battery/electrode_sheet.py new file mode 100644 index 00000000..e86af24a --- /dev/null +++ b/unilabos/resources/battery/electrode_sheet.py @@ -0,0 +1,179 @@ +from typing import Any, Dict, Optional, TypedDict + +from pylabrobot.resources import Resource as ResourcePLR +from pylabrobot.resources import Container + + +electrode_colors = { + "PositiveCan": "#ff0000", + "PositiveElectrode": "#cc3333", + "NegativeCan": "#000000", + "NegativeElectrode": "#666666", + "SpringWasher": "#8b7355", + "FlatWasher": "a9a9a9", + "AluminumFoil": "#ffcccc", + "Battery": "#00ff00", +} + +class ElectrodeSheetState(TypedDict): + mass: float # 质量 (g) + material_type: str # 材料类型(铜、铝、不锈钢、弹簧钢等) + color: str # 材料类型对应的颜色 + + +class ElectrodeSheet(ResourcePLR): + """极片类 - 包含正负极片、隔膜、弹片、垫片、铝箔等所有片状材料""" + + 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 + + +def PositiveCan(name: str) -> ElectrodeSheet: + """创建正极壳""" + sheet = ElectrodeSheet(name=name, size_x=12, size_y=12, size_z=3.0, model="PositiveCan") + sheet.load_state({"material_type": "aluminum", "color": electrode_colors["PositiveCan"]}) + return sheet + + +def PositiveElectrode(name: str) -> ElectrodeSheet: + """创建正极片""" + sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.1, model="PositiveElectrode") + sheet.load_state({"material_type": "positive_electrode", "color": electrode_colors["PositiveElectrode"]}) + return sheet + + +def NegativeCan(name: str) -> ElectrodeSheet: + """创建负极壳""" + sheet = ElectrodeSheet(name=name, size_x=12, size_y=12, size_z=2.0, model="NegativeCan") + sheet.load_state({"material_type": "steel", "color": electrode_colors["NegativeCan"]}) + return sheet + + +def NegativeElectrode(name: str) -> ElectrodeSheet: + """创建负极片""" + sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.1, model="NegativeElectrode") + sheet.load_state({"material_type": "negative_electrode", "color": electrode_colors["NegativeElectrode"]}) + return sheet + + +def SpringWasher(name: str) -> ElectrodeSheet: + """创建弹片""" + sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.5, model="SpringWasher") + sheet.load_state({"material_type": "spring_steel", "color": electrode_colors["SpringWasher"]}) + return sheet + + +def FlatWasher(name: str) -> ElectrodeSheet: + """创建垫片""" + sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.2, model="FlatWasher") + sheet.load_state({"material_type": "steel", "color": electrode_colors["FlatWasher"]}) + return sheet + + +def AluminumFoil(name: str) -> ElectrodeSheet: + """创建铝箔""" + sheet = ElectrodeSheet(name=name, size_x=10, size_y=10, size_z=0.05, model="AluminumFoil") + sheet.load_state({"material_type": "aluminum", "color": electrode_colors["AluminumFoil"]}) + return sheet + + +class BatteryState(TypedDict): + color: str # 材料类型对应的颜色 + electrolyte_name: str + data_electrolyte_code: str + open_circuit_voltage: float + assembly_pressure: float + electrolyte_volume: float + + info: Optional[str] # 附加信息 + + +class Battery(Container): + """电池类 - 包含组装好的电池""" + + def __init__( + self, + name: str = "电池", + size_x=12, + size_y=12, + size_z=6, + category: str = "battery", + 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: BatteryState = BatteryState( + color=electrode_colors["Battery"], + electrolyte_name="无", + data_electrolyte_code="", + open_circuit_voltage=0.0, + assembly_pressure=0.0, + electrolyte_volume=0.0, + info=None + ) + + 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 \ No newline at end of file diff --git a/unilabos/resources/battery/magazine.py b/unilabos/resources/battery/magazine.py new file mode 100644 index 00000000..f8d24447 --- /dev/null +++ b/unilabos/resources/battery/magazine.py @@ -0,0 +1,335 @@ +from typing import Dict, List, Optional, OrderedDict, Union, Callable +import math + +from pylabrobot.resources.coordinate import Coordinate +from pylabrobot.resources import Resource, ResourceStack, ItemizedResource +from pylabrobot.resources.carrier import create_homogeneous_resources + +from unilabos.resources.battery.electrode_sheet import ( + PositiveCan, PositiveElectrode, + NegativeCan, NegativeElectrode, + SpringWasher, FlatWasher, + AluminumFoil, + Battery +) + + +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 + + @property + def size_x(self) -> float: + return self.get_size_x() + + @property + def size_y(self) -> float: + return self.get_size_y() + + @property + def size_z(self) -> float: + return self.get_size_z() + + +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], + klasses: Optional[List[Callable[[str], str]]] = None, + 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: 洞位坐标列表 + klasses: 每个洞位中极片的类列表 + hole_diameter: 洞直径 (mm) + hole_depth: 洞深度 (mm) + max_sheets_per_hole: 每个洞位最大极片数量 + category: 类别 + model: 型号 + """ + for loc in locations: + loc.x -= hole_diameter / 2 + loc.y -= hole_diameter / 2 + + # 创建洞位 + _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())) + + holder = 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, + ) + + if klasses is not None: + for i, klass in enumerate(klasses): + hole_key = keys[i] + hole = holder.children[i] + for j in reversed(range(max_sheets_per_hole)): + item_name = f"{hole_key}_sheet{j+1}" + item = klass(name=item_name) + hole.assign_child_resource(item) + return holder + + +def MagazineHolder_6_Cathode( + 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孔子弹夹 - 六边形排布""" + 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, + klasses=[FlatWasher, PositiveCan, PositiveCan, FlatWasher, PositiveCan, PositiveCan], + hole_diameter=hole_diameter, + hole_depth=hole_depth, + max_sheets_per_hole=max_sheets_per_hole, + category="magazine_holder", + model="MagazineHolder_6_Cathode", + ) + + +def MagazineHolder_6_Anode( + 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孔子弹夹 - 六边形排布""" + 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, + klasses=[SpringWasher, NegativeCan, NegativeCan, SpringWasher, NegativeCan, NegativeCan], + hole_diameter=hole_diameter, + hole_depth=hole_depth, + max_sheets_per_hole=max_sheets_per_hole, + category="magazine_holder", + model="MagazineHolder_6_Anode", + ) + + +def MagazineHolder_6_Battery( + 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孔子弹夹 - 六边形排布""" + 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, + klasses=None, # 初始化时,不放入装好的电池 + hole_diameter=hole_diameter, + hole_depth=hole_depth, + max_sheets_per_hole=max_sheets_per_hole, + category="magazine_holder", + model="MagazineHolder_6_Battery", + ) + + +def MagazineHolder_4_Cathode( + name: str, +) -> MagazineHolder: + """创建4孔子弹夹 - 正方形四角排布""" + 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 + + # 计算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, + klasses=[AluminumFoil, PositiveElectrode, PositiveElectrode, PositiveElectrode], + hole_diameter=hole_diameter, + hole_depth=hole_depth, + max_sheets_per_hole=max_sheets_per_hole, + category="magazine_holder", + model="MagazineHolder_4_Cathode", + ) 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,