diff --git a/bioyond_yihua_YB.json b/bioyond_yihua_YB.json index 1cd7f00f..c38179d9 100644 --- a/bioyond_yihua_YB.json +++ b/bioyond_yihua_YB.json @@ -17,29 +17,16 @@ { "id": "BatteryStation", "name": "扣电组装工作站", - "children": [ - "coin_cell_deck" - ], + "children": [], "parent": null, "type": "device", "class": "bettery_station_registry", - "position": { - "x": 600, - "y": 400, - "z": 0 - }, "config": { - "debug_mode": true, - "_comment": "protocol_type接外部工站固定写法字段,一般为空,deck写法也固定", + "debug_mode": false, + "protocol_type": [], - "deck": { - "data": { - "_resource_child_name": "coin_cell_deck", - "_resource_type": "unilabos.devices.workstation.coin_cell_assembly.button_battery_station:CoincellDeck" - } - }, - - "address": "192.168.1.20", + "deck": "unilabos.devices.workstation.coin_cell_assembly.button_battery_station:CoincellDeck", + "address": "172.21.32.20", "port": 502 }, "data": {} diff --git a/button_battery_station_resources_unilab.json b/button_battery_station_resources_unilab.json new file mode 100644 index 00000000..f60d1c79 --- /dev/null +++ b/button_battery_station_resources_unilab.json @@ -0,0 +1,2521 @@ +{ + "nodes": [ + { + "id": "coin_cell_deck", + "name": "coin_cell_deck", + "sample_id": null, + "children": [ + "liaopan1", + "liaopan2", + "\u7535\u6c60\u6599\u76d8" + ], + "parent": null, + "type": "coin_cell_deck", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "CoincellDeck", + "size_x": 1000, + "size_y": 1000, + "size_z": 900, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "coin_cell_deck", + "barcode": null + }, + "data": {} + }, + { + "id": "liaopan1", + "name": "liaopan1", + "sample_id": null, + "children": [ + "liaopan1_materialhole_0_0", + "liaopan1_materialhole_0_1", + "liaopan1_materialhole_0_2", + "liaopan1_materialhole_0_3", + "liaopan1_materialhole_1_0", + "liaopan1_materialhole_1_1", + "liaopan1_materialhole_1_2", + "liaopan1_materialhole_1_3", + "liaopan1_materialhole_2_0", + "liaopan1_materialhole_2_1", + "liaopan1_materialhole_2_2", + "liaopan1_materialhole_2_3", + "liaopan1_materialhole_3_0", + "liaopan1_materialhole_3_1", + "liaopan1_materialhole_3_2", + "liaopan1_materialhole_3_3" + ], + "parent": "coin_cell_deck", + "type": "material_plate", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "MaterialPlate", + "size_x": 120.8, + "size_y": 120.5, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_plate", + "model": null, + "barcode": null, + "ordering": { + "A1": "liaopan1_materialhole_0_0", + "B1": "liaopan1_materialhole_0_1", + "C1": "liaopan1_materialhole_0_2", + "D1": "liaopan1_materialhole_0_3", + "A2": "liaopan1_materialhole_1_0", + "B2": "liaopan1_materialhole_1_1", + "C2": "liaopan1_materialhole_1_2", + "D2": "liaopan1_materialhole_1_3", + "A3": "liaopan1_materialhole_2_0", + "B3": "liaopan1_materialhole_2_1", + "C3": "liaopan1_materialhole_2_2", + "D3": "liaopan1_materialhole_2_3", + "A4": "liaopan1_materialhole_3_0", + "B4": "liaopan1_materialhole_3_1", + "C4": "liaopan1_materialhole_3_2", + "D4": "liaopan1_materialhole_3_3" + } + }, + "data": {} + }, + { + "id": "liaopan1_materialhole_0_0", + "name": "liaopan1_materialhole_0_0", + "sample_id": null, + "children": [], + "parent": "liaopan1", + "type": "material_hole", + "class": "", + "position": { + "x": 12.4, + "y": 84.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_materialhole_0_1", + "name": "liaopan1_materialhole_0_1", + "sample_id": null, + "children": [], + "parent": "liaopan1", + "type": "material_hole", + "class": "", + "position": { + "x": 12.4, + "y": 60.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_materialhole_0_2", + "name": "liaopan1_materialhole_0_2", + "sample_id": null, + "children": [], + "parent": "liaopan1", + "type": "material_hole", + "class": "", + "position": { + "x": 12.4, + "y": 36.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_materialhole_0_3", + "name": "liaopan1_materialhole_0_3", + "sample_id": null, + "children": [], + "parent": "liaopan1", + "type": "material_hole", + "class": "", + "position": { + "x": 12.4, + "y": 12.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_materialhole_1_0", + "name": "liaopan1_materialhole_1_0", + "sample_id": null, + "children": [], + "parent": "liaopan1", + "type": "material_hole", + "class": "", + "position": { + "x": 36.4, + "y": 84.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_materialhole_1_1", + "name": "liaopan1_materialhole_1_1", + "sample_id": null, + "children": [], + "parent": "liaopan1", + "type": "material_hole", + "class": "", + "position": { + "x": 36.4, + "y": 60.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_materialhole_1_2", + "name": "liaopan1_materialhole_1_2", + "sample_id": null, + "children": [], + "parent": "liaopan1", + "type": "material_hole", + "class": "", + "position": { + "x": 36.4, + "y": 36.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_materialhole_1_3", + "name": "liaopan1_materialhole_1_3", + "sample_id": null, + "children": [], + "parent": "liaopan1", + "type": "material_hole", + "class": "", + "position": { + "x": 36.4, + "y": 12.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_materialhole_2_0", + "name": "liaopan1_materialhole_2_0", + "sample_id": null, + "children": [], + "parent": "liaopan1", + "type": "material_hole", + "class": "", + "position": { + "x": 60.4, + "y": 84.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_materialhole_2_1", + "name": "liaopan1_materialhole_2_1", + "sample_id": null, + "children": [], + "parent": "liaopan1", + "type": "material_hole", + "class": "", + "position": { + "x": 60.4, + "y": 60.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_materialhole_2_2", + "name": "liaopan1_materialhole_2_2", + "sample_id": null, + "children": [], + "parent": "liaopan1", + "type": "material_hole", + "class": "", + "position": { + "x": 60.4, + "y": 36.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_materialhole_2_3", + "name": "liaopan1_materialhole_2_3", + "sample_id": null, + "children": [], + "parent": "liaopan1", + "type": "material_hole", + "class": "", + "position": { + "x": 60.4, + "y": 12.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_materialhole_3_0", + "name": "liaopan1_materialhole_3_0", + "sample_id": null, + "children": [], + "parent": "liaopan1", + "type": "material_hole", + "class": "", + "position": { + "x": 84.4, + "y": 84.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_materialhole_3_1", + "name": "liaopan1_materialhole_3_1", + "sample_id": null, + "children": [], + "parent": "liaopan1", + "type": "material_hole", + "class": "", + "position": { + "x": 84.4, + "y": 60.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_materialhole_3_2", + "name": "liaopan1_materialhole_3_2", + "sample_id": null, + "children": [], + "parent": "liaopan1", + "type": "material_hole", + "class": "", + "position": { + "x": 84.4, + "y": 36.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan1_materialhole_3_3", + "name": "liaopan1_materialhole_3_3", + "sample_id": null, + "children": [], + "parent": "liaopan1", + "type": "material_hole", + "class": "", + "position": { + "x": 84.4, + "y": 12.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "liaopan2", + "name": "liaopan2", + "sample_id": null, + "children": [ + "liaopan2_materialhole_0_0", + "liaopan2_materialhole_0_1", + "liaopan2_materialhole_0_2", + "liaopan2_materialhole_0_3", + "liaopan2_materialhole_1_0", + "liaopan2_materialhole_1_1", + "liaopan2_materialhole_1_2", + "liaopan2_materialhole_1_3", + "liaopan2_materialhole_2_0", + "liaopan2_materialhole_2_1", + "liaopan2_materialhole_2_2", + "liaopan2_materialhole_2_3", + "liaopan2_materialhole_3_0", + "liaopan2_materialhole_3_1", + "liaopan2_materialhole_3_2", + "liaopan2_materialhole_3_3" + ], + "parent": "coin_cell_deck", + "type": "material_plate", + "class": "", + "position": { + "x": 500, + "y": 0, + "z": 0 + }, + "config": { + "type": "MaterialPlate", + "size_x": 120.8, + "size_y": 120.5, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_plate", + "model": null, + "barcode": null, + "ordering": { + "A1": "liaopan2_materialhole_0_0", + "B1": "liaopan2_materialhole_0_1", + "C1": "liaopan2_materialhole_0_2", + "D1": "liaopan2_materialhole_0_3", + "A2": "liaopan2_materialhole_1_0", + "B2": "liaopan2_materialhole_1_1", + "C2": "liaopan2_materialhole_1_2", + "D2": "liaopan2_materialhole_1_3", + "A3": "liaopan2_materialhole_2_0", + "B3": "liaopan2_materialhole_2_1", + "C3": "liaopan2_materialhole_2_2", + "D3": "liaopan2_materialhole_2_3", + "A4": "liaopan2_materialhole_3_0", + "B4": "liaopan2_materialhole_3_1", + "C4": "liaopan2_materialhole_3_2", + "D4": "liaopan2_materialhole_3_3" + } + }, + "data": {} + }, + { + "id": "liaopan2_materialhole_0_0", + "name": "liaopan2_materialhole_0_0", + "sample_id": null, + "children": [ + "jipian1_0" + ], + "parent": "liaopan2", + "type": "material_hole", + "class": "", + "position": { + "x": 12.4, + "y": 84.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "jipian1_0", + "name": "jipian1_0", + "sample_id": null, + "children": [], + "parent": "liaopan2_materialhole_0_0", + "type": "electrode_sheet", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan2_materialhole_0_1", + "name": "liaopan2_materialhole_0_1", + "sample_id": null, + "children": [ + "jipian1_1" + ], + "parent": "liaopan2", + "type": "material_hole", + "class": "", + "position": { + "x": 12.4, + "y": 60.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "jipian1_1", + "name": "jipian1_1", + "sample_id": null, + "children": [], + "parent": "liaopan2_materialhole_0_1", + "type": "electrode_sheet", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan2_materialhole_0_2", + "name": "liaopan2_materialhole_0_2", + "sample_id": null, + "children": [ + "jipian1_2" + ], + "parent": "liaopan2", + "type": "material_hole", + "class": "", + "position": { + "x": 12.4, + "y": 36.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "jipian1_2", + "name": "jipian1_2", + "sample_id": null, + "children": [], + "parent": "liaopan2_materialhole_0_2", + "type": "electrode_sheet", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan2_materialhole_0_3", + "name": "liaopan2_materialhole_0_3", + "sample_id": null, + "children": [ + "jipian1_3" + ], + "parent": "liaopan2", + "type": "material_hole", + "class": "", + "position": { + "x": 12.4, + "y": 12.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "jipian1_3", + "name": "jipian1_3", + "sample_id": null, + "children": [], + "parent": "liaopan2_materialhole_0_3", + "type": "electrode_sheet", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan2_materialhole_1_0", + "name": "liaopan2_materialhole_1_0", + "sample_id": null, + "children": [ + "jipian1_4" + ], + "parent": "liaopan2", + "type": "material_hole", + "class": "", + "position": { + "x": 36.4, + "y": 84.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "jipian1_4", + "name": "jipian1_4", + "sample_id": null, + "children": [], + "parent": "liaopan2_materialhole_1_0", + "type": "electrode_sheet", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan2_materialhole_1_1", + "name": "liaopan2_materialhole_1_1", + "sample_id": null, + "children": [ + "jipian1_5" + ], + "parent": "liaopan2", + "type": "material_hole", + "class": "", + "position": { + "x": 36.4, + "y": 60.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "jipian1_5", + "name": "jipian1_5", + "sample_id": null, + "children": [], + "parent": "liaopan2_materialhole_1_1", + "type": "electrode_sheet", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan2_materialhole_1_2", + "name": "liaopan2_materialhole_1_2", + "sample_id": null, + "children": [ + "jipian1_6" + ], + "parent": "liaopan2", + "type": "material_hole", + "class": "", + "position": { + "x": 36.4, + "y": 36.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "jipian1_6", + "name": "jipian1_6", + "sample_id": null, + "children": [], + "parent": "liaopan2_materialhole_1_2", + "type": "electrode_sheet", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan2_materialhole_1_3", + "name": "liaopan2_materialhole_1_3", + "sample_id": null, + "children": [ + "jipian1_7" + ], + "parent": "liaopan2", + "type": "material_hole", + "class": "", + "position": { + "x": 36.4, + "y": 12.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "jipian1_7", + "name": "jipian1_7", + "sample_id": null, + "children": [], + "parent": "liaopan2_materialhole_1_3", + "type": "electrode_sheet", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan2_materialhole_2_0", + "name": "liaopan2_materialhole_2_0", + "sample_id": null, + "children": [ + "jipian1_8" + ], + "parent": "liaopan2", + "type": "material_hole", + "class": "", + "position": { + "x": 60.4, + "y": 84.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "jipian1_8", + "name": "jipian1_8", + "sample_id": null, + "children": [], + "parent": "liaopan2_materialhole_2_0", + "type": "electrode_sheet", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan2_materialhole_2_1", + "name": "liaopan2_materialhole_2_1", + "sample_id": null, + "children": [ + "jipian1_9" + ], + "parent": "liaopan2", + "type": "material_hole", + "class": "", + "position": { + "x": 60.4, + "y": 60.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "jipian1_9", + "name": "jipian1_9", + "sample_id": null, + "children": [], + "parent": "liaopan2_materialhole_2_1", + "type": "electrode_sheet", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan2_materialhole_2_2", + "name": "liaopan2_materialhole_2_2", + "sample_id": null, + "children": [ + "jipian1_10" + ], + "parent": "liaopan2", + "type": "material_hole", + "class": "", + "position": { + "x": 60.4, + "y": 36.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "jipian1_10", + "name": "jipian1_10", + "sample_id": null, + "children": [], + "parent": "liaopan2_materialhole_2_2", + "type": "electrode_sheet", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan2_materialhole_2_3", + "name": "liaopan2_materialhole_2_3", + "sample_id": null, + "children": [ + "jipian1_11" + ], + "parent": "liaopan2", + "type": "material_hole", + "class": "", + "position": { + "x": 60.4, + "y": 12.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "jipian1_11", + "name": "jipian1_11", + "sample_id": null, + "children": [], + "parent": "liaopan2_materialhole_2_3", + "type": "electrode_sheet", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan2_materialhole_3_0", + "name": "liaopan2_materialhole_3_0", + "sample_id": null, + "children": [ + "jipian1_12" + ], + "parent": "liaopan2", + "type": "material_hole", + "class": "", + "position": { + "x": 84.4, + "y": 84.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "jipian1_12", + "name": "jipian1_12", + "sample_id": null, + "children": [], + "parent": "liaopan2_materialhole_3_0", + "type": "electrode_sheet", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan2_materialhole_3_1", + "name": "liaopan2_materialhole_3_1", + "sample_id": null, + "children": [ + "jipian1_13" + ], + "parent": "liaopan2", + "type": "material_hole", + "class": "", + "position": { + "x": 84.4, + "y": 60.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "jipian1_13", + "name": "jipian1_13", + "sample_id": null, + "children": [], + "parent": "liaopan2_materialhole_3_1", + "type": "electrode_sheet", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan2_materialhole_3_2", + "name": "liaopan2_materialhole_3_2", + "sample_id": null, + "children": [ + "jipian1_14" + ], + "parent": "liaopan2", + "type": "material_hole", + "class": "", + "position": { + "x": 84.4, + "y": 36.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "jipian1_14", + "name": "jipian1_14", + "sample_id": null, + "children": [], + "parent": "liaopan2_materialhole_3_2", + "type": "electrode_sheet", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "liaopan2_materialhole_3_3", + "name": "liaopan2_materialhole_3_3", + "sample_id": null, + "children": [ + "jipian1_15" + ], + "parent": "liaopan2", + "type": "material_hole", + "class": "", + "position": { + "x": 84.4, + "y": 12.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "jipian1_15", + "name": "jipian1_15", + "sample_id": null, + "children": [], + "parent": "liaopan2_materialhole_3_3", + "type": "electrode_sheet", + "class": "", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "ElectrodeSheet", + "size_x": 12, + "size_y": 12, + "size_z": 0.1, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "electrode_sheet", + "model": null, + "barcode": null + }, + "data": { + "diameter": 14, + "thickness": 0.1, + "mass": 0.5, + "material_type": "copper", + "info": null + } + }, + { + "id": "\u7535\u6c60\u6599\u76d8", + "name": "\u7535\u6c60\u6599\u76d8", + "sample_id": null, + "children": [ + "\u7535\u6c60\u6599\u76d8_materialhole_0_0", + "\u7535\u6c60\u6599\u76d8_materialhole_0_1", + "\u7535\u6c60\u6599\u76d8_materialhole_0_2", + "\u7535\u6c60\u6599\u76d8_materialhole_0_3", + "\u7535\u6c60\u6599\u76d8_materialhole_1_0", + "\u7535\u6c60\u6599\u76d8_materialhole_1_1", + "\u7535\u6c60\u6599\u76d8_materialhole_1_2", + "\u7535\u6c60\u6599\u76d8_materialhole_1_3", + "\u7535\u6c60\u6599\u76d8_materialhole_2_0", + "\u7535\u6c60\u6599\u76d8_materialhole_2_1", + "\u7535\u6c60\u6599\u76d8_materialhole_2_2", + "\u7535\u6c60\u6599\u76d8_materialhole_2_3", + "\u7535\u6c60\u6599\u76d8_materialhole_3_0", + "\u7535\u6c60\u6599\u76d8_materialhole_3_1", + "\u7535\u6c60\u6599\u76d8_materialhole_3_2", + "\u7535\u6c60\u6599\u76d8_materialhole_3_3" + ], + "parent": "coin_cell_deck", + "type": "material_plate", + "class": "", + "position": { + "x": 100, + "y": 100, + "z": 0 + }, + "config": { + "type": "MaterialPlate", + "size_x": 120.8, + "size_y": 160.5, + "size_z": 10.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_plate", + "model": null, + "barcode": null, + "ordering": { + "A1": "\u7535\u6c60\u6599\u76d8_materialhole_0_0", + "B1": "\u7535\u6c60\u6599\u76d8_materialhole_0_1", + "C1": "\u7535\u6c60\u6599\u76d8_materialhole_0_2", + "D1": "\u7535\u6c60\u6599\u76d8_materialhole_0_3", + "A2": "\u7535\u6c60\u6599\u76d8_materialhole_1_0", + "B2": "\u7535\u6c60\u6599\u76d8_materialhole_1_1", + "C2": "\u7535\u6c60\u6599\u76d8_materialhole_1_2", + "D2": "\u7535\u6c60\u6599\u76d8_materialhole_1_3", + "A3": "\u7535\u6c60\u6599\u76d8_materialhole_2_0", + "B3": "\u7535\u6c60\u6599\u76d8_materialhole_2_1", + "C3": "\u7535\u6c60\u6599\u76d8_materialhole_2_2", + "D3": "\u7535\u6c60\u6599\u76d8_materialhole_2_3", + "A4": "\u7535\u6c60\u6599\u76d8_materialhole_3_0", + "B4": "\u7535\u6c60\u6599\u76d8_materialhole_3_1", + "C4": "\u7535\u6c60\u6599\u76d8_materialhole_3_2", + "D4": "\u7535\u6c60\u6599\u76d8_materialhole_3_3" + } + }, + "data": {} + }, + { + "id": "\u7535\u6c60\u6599\u76d8_materialhole_0_0", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_0_0", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "material_hole", + "class": "", + "position": { + "x": 12.4, + "y": 104.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "\u7535\u6c60\u6599\u76d8_materialhole_0_1", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_0_1", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "material_hole", + "class": "", + "position": { + "x": 12.4, + "y": 80.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "\u7535\u6c60\u6599\u76d8_materialhole_0_2", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_0_2", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "material_hole", + "class": "", + "position": { + "x": 12.4, + "y": 56.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "\u7535\u6c60\u6599\u76d8_materialhole_0_3", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_0_3", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "material_hole", + "class": "", + "position": { + "x": 12.4, + "y": 32.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "\u7535\u6c60\u6599\u76d8_materialhole_1_0", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_1_0", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "material_hole", + "class": "", + "position": { + "x": 36.4, + "y": 104.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "\u7535\u6c60\u6599\u76d8_materialhole_1_1", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_1_1", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "material_hole", + "class": "", + "position": { + "x": 36.4, + "y": 80.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "\u7535\u6c60\u6599\u76d8_materialhole_1_2", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_1_2", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "material_hole", + "class": "", + "position": { + "x": 36.4, + "y": 56.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "\u7535\u6c60\u6599\u76d8_materialhole_1_3", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_1_3", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "material_hole", + "class": "", + "position": { + "x": 36.4, + "y": 32.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "\u7535\u6c60\u6599\u76d8_materialhole_2_0", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_2_0", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "material_hole", + "class": "", + "position": { + "x": 60.4, + "y": 104.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "\u7535\u6c60\u6599\u76d8_materialhole_2_1", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_2_1", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "material_hole", + "class": "", + "position": { + "x": 60.4, + "y": 80.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "\u7535\u6c60\u6599\u76d8_materialhole_2_2", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_2_2", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "material_hole", + "class": "", + "position": { + "x": 60.4, + "y": 56.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "\u7535\u6c60\u6599\u76d8_materialhole_2_3", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_2_3", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "material_hole", + "class": "", + "position": { + "x": 60.4, + "y": 32.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "\u7535\u6c60\u6599\u76d8_materialhole_3_0", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_3_0", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "material_hole", + "class": "", + "position": { + "x": 84.4, + "y": 104.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "\u7535\u6c60\u6599\u76d8_materialhole_3_1", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_3_1", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "material_hole", + "class": "", + "position": { + "x": 84.4, + "y": 80.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "\u7535\u6c60\u6599\u76d8_materialhole_3_2", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_3_2", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "material_hole", + "class": "", + "position": { + "x": 84.4, + "y": 56.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + }, + { + "id": "\u7535\u6c60\u6599\u76d8_materialhole_3_3", + "name": "\u7535\u6c60\u6599\u76d8_materialhole_3_3", + "sample_id": null, + "children": [], + "parent": "\u7535\u6c60\u6599\u76d8", + "type": "material_hole", + "class": "", + "position": { + "x": 84.4, + "y": 32.25, + "z": 10.0 + }, + "config": { + "type": "MaterialHole", + "size_x": 16, + "size_y": 16, + "size_z": 16, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "material_hole", + "model": null, + "barcode": null + }, + "data": { + "diameter": 20, + "depth": 10, + "max_sheets": 1, + "info": null + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/scripts/workflow.py b/scripts/workflow.py new file mode 100644 index 00000000..be7bbd1e --- /dev/null +++ b/scripts/workflow.py @@ -0,0 +1,695 @@ +import json +import logging +import traceback +import uuid +import xml.etree.ElementTree as ET +from typing import Any, Dict, List + +import networkx as nx +import matplotlib.pyplot as plt +import requests + +logger = logging.getLogger(__name__) + + +class SimpleGraph: + """简单的有向图实现,用于构建工作流图""" + + def __init__(self): + self.nodes = {} + self.edges = [] + + def add_node(self, node_id, **attrs): + """添加节点""" + self.nodes[node_id] = attrs + + def add_edge(self, source, target, **attrs): + """添加边""" + edge = {"source": source, "target": target, **attrs} + self.edges.append(edge) + + def to_dict(self): + """转换为工作流图格式""" + nodes_list = [] + for node_id, attrs in self.nodes.items(): + node_attrs = attrs.copy() + params = node_attrs.pop("parameters", {}) or {} + node_attrs.update(params) + nodes_list.append({"id": node_id, **node_attrs}) + + return { + "directed": True, + "multigraph": False, + "graph": {}, + "nodes": nodes_list, + "links": self.edges, + } + + +def extract_json_from_markdown(text: str) -> str: + """从markdown代码块中提取JSON""" + text = text.strip() + if text.startswith("```json\n"): + text = text[8:] + if text.startswith("```\n"): + text = text[4:] + if text.endswith("\n```"): + text = text[:-4] + return text + + +def convert_to_type(val: str) -> Any: + """将字符串值转换为适当的数据类型""" + if val == "True": + return True + if val == "False": + return False + if val == "?": + return None + if val.endswith(" g"): + return float(val.split(" ")[0]) + if val.endswith("mg"): + return float(val.split("mg")[0]) + elif val.endswith("mmol"): + return float(val.split("mmol")[0]) / 1000 + elif val.endswith("mol"): + return float(val.split("mol")[0]) + elif val.endswith("ml"): + return float(val.split("ml")[0]) + elif val.endswith("RPM"): + return float(val.split("RPM")[0]) + elif val.endswith(" °C"): + return float(val.split(" ")[0]) + elif val.endswith(" %"): + return float(val.split(" ")[0]) + return val + + +def refactor_data(data: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + """统一的数据重构函数,根据操作类型自动选择模板""" + refactored_data = [] + + # 定义操作映射,包含生物实验和有机化学的所有操作 + OPERATION_MAPPING = { + # 生物实验操作 + "transfer_liquid": "SynBioFactory-liquid_handler.prcxi-transfer_liquid", + "transfer": "SynBioFactory-liquid_handler.biomek-transfer", + "incubation": "SynBioFactory-liquid_handler.biomek-incubation", + "move_labware": "SynBioFactory-liquid_handler.biomek-move_labware", + "oscillation": "SynBioFactory-liquid_handler.biomek-oscillation", + # 有机化学操作 + "HeatChillToTemp": "SynBioFactory-workstation-HeatChillProtocol", + "StopHeatChill": "SynBioFactory-workstation-HeatChillStopProtocol", + "StartHeatChill": "SynBioFactory-workstation-HeatChillStartProtocol", + "HeatChill": "SynBioFactory-workstation-HeatChillProtocol", + "Dissolve": "SynBioFactory-workstation-DissolveProtocol", + "Transfer": "SynBioFactory-workstation-TransferProtocol", + "Evaporate": "SynBioFactory-workstation-EvaporateProtocol", + "Recrystallize": "SynBioFactory-workstation-RecrystallizeProtocol", + "Filter": "SynBioFactory-workstation-FilterProtocol", + "Dry": "SynBioFactory-workstation-DryProtocol", + "Add": "SynBioFactory-workstation-AddProtocol", + } + + UNSUPPORTED_OPERATIONS = ["Purge", "Wait", "Stir", "ResetHandling"] + + for step in data: + operation = step.get("action") + if not operation or operation in UNSUPPORTED_OPERATIONS: + continue + + # 处理重复操作 + if operation == "Repeat": + times = step.get("times", step.get("parameters", {}).get("times", 1)) + sub_steps = step.get("steps", step.get("parameters", {}).get("steps", [])) + for i in range(int(times)): + sub_data = refactor_data(sub_steps) + refactored_data.extend(sub_data) + continue + + # 获取模板名称 + template = OPERATION_MAPPING.get(operation) + if not template: + # 自动推断模板类型 + if operation.lower() in ["transfer", "incubation", "move_labware", "oscillation"]: + template = f"SynBioFactory-liquid_handler.biomek-{operation}" + else: + template = f"SynBioFactory-workstation-{operation}Protocol" + + # 创建步骤数据 + step_data = { + "template": template, + "description": step.get("description", step.get("purpose", f"{operation} operation")), + "lab_node_type": "Device", + "parameters": step.get("parameters", step.get("action_args", {})), + } + refactored_data.append(step_data) + + return refactored_data + + +def build_protocol_graph( + labware_info: List[Dict[str, Any]], protocol_steps: List[Dict[str, Any]], workstation_name: str +) -> SimpleGraph: + """统一的协议图构建函数,根据设备类型自动选择构建逻辑""" + G = SimpleGraph() + resource_last_writer = {} + LAB_NAME = "SynBioFactory" + + protocol_steps = refactor_data(protocol_steps) + + # 检查协议步骤中的模板来判断协议类型 + has_biomek_template = any( + ("biomek" in step.get("template", "")) or ("prcxi" in step.get("template", "")) + for step in protocol_steps + ) + + if has_biomek_template: + # 生物实验协议图构建 + for labware_id, labware in labware_info.items(): + node_id = str(uuid.uuid4()) + + labware_attrs = labware.copy() + labware_id = labware_attrs.pop("id", labware_attrs.get("name", f"labware_{uuid.uuid4()}")) + labware_attrs["description"] = labware_id + labware_attrs["lab_node_type"] = ( + "Reagent" if "Plate" in str(labware_id) else "Labware" if "Rack" in str(labware_id) else "Sample" + ) + labware_attrs["device_id"] = workstation_name + + G.add_node(node_id, template=f"{LAB_NAME}-host_node-create_resource", **labware_attrs) + resource_last_writer[labware_id] = f"{node_id}:labware" + + # 处理协议步骤 + prev_node = None + for i, step in enumerate(protocol_steps): + node_id = str(uuid.uuid4()) + G.add_node(node_id, **step) + + # 添加控制流边 + if prev_node is not None: + G.add_edge(prev_node, node_id, source_port="ready", target_port="ready") + prev_node = node_id + + # 处理物料流 + params = step.get("parameters", {}) + if "sources" in params and params["sources"] in resource_last_writer: + source_node, source_port = resource_last_writer[params["sources"]].split(":") + G.add_edge(source_node, node_id, source_port=source_port, target_port="labware") + + if "targets" in params: + resource_last_writer[params["targets"]] = f"{node_id}:labware" + + # 添加协议结束节点 + end_id = str(uuid.uuid4()) + G.add_node(end_id, template=f"{LAB_NAME}-liquid_handler.biomek-run_protocol") + if prev_node is not None: + G.add_edge(prev_node, end_id, source_port="ready", target_port="ready") + + else: + # 有机化学协议图构建 + WORKSTATION_ID = workstation_name + + # 为所有labware创建资源节点 + for item_id, item in labware_info.items(): + # item_id = item.get("id") or item.get("name", f"item_{uuid.uuid4()}") + node_id = str(uuid.uuid4()) + + # 判断节点类型 + if item.get("type") == "hardware" or "reactor" in str(item_id).lower(): + if "reactor" not in str(item_id).lower(): + continue + lab_node_type = "Sample" + description = f"Prepare Reactor: {item_id}" + liquid_type = [] + liquid_volume = [] + else: + lab_node_type = "Reagent" + description = f"Add Reagent to Flask: {item_id}" + liquid_type = [item_id] + liquid_volume = [1e5] + + G.add_node( + node_id, + template=f"{LAB_NAME}-host_node-create_resource", + description=description, + lab_node_type=lab_node_type, + res_id=item_id, + device_id=WORKSTATION_ID, + class_name="container", + parent=WORKSTATION_ID, + bind_locations={"x": 0.0, "y": 0.0, "z": 0.0}, + liquid_input_slot=[-1], + liquid_type=liquid_type, + liquid_volume=liquid_volume, + slot_on_deck="", + role=item.get("role", ""), + ) + resource_last_writer[item_id] = f"{node_id}:labware" + + last_control_node_id = None + + # 处理协议步骤 + for step in protocol_steps: + node_id = str(uuid.uuid4()) + G.add_node(node_id, **step) + + # 控制流 + if last_control_node_id is not None: + G.add_edge(last_control_node_id, node_id, source_port="ready", target_port="ready") + last_control_node_id = node_id + + # 物料流 + params = step.get("parameters", {}) + input_resources = { + "Vessel": params.get("vessel"), + "ToVessel": params.get("to_vessel"), + "FromVessel": params.get("from_vessel"), + "reagent": params.get("reagent"), + "solvent": params.get("solvent"), + "compound": params.get("compound"), + "sources": params.get("sources"), + "targets": params.get("targets"), + } + + for target_port, resource_name in input_resources.items(): + if resource_name and resource_name in resource_last_writer: + source_node, source_port = resource_last_writer[resource_name].split(":") + G.add_edge(source_node, node_id, source_port=source_port, target_port=target_port) + + output_resources = { + "VesselOut": params.get("vessel"), + "FromVesselOut": params.get("from_vessel"), + "ToVesselOut": params.get("to_vessel"), + "FiltrateOut": params.get("filtrate_vessel"), + "reagent": params.get("reagent"), + "solvent": params.get("solvent"), + "compound": params.get("compound"), + "sources_out": params.get("sources"), + "targets_out": params.get("targets"), + } + + for source_port, resource_name in output_resources.items(): + if resource_name: + resource_last_writer[resource_name] = f"{node_id}:{source_port}" + + return G + + +def draw_protocol_graph(protocol_graph: SimpleGraph, output_path: str): + """ + (辅助功能) 使用 networkx 和 matplotlib 绘制协议工作流图,用于可视化。 + """ + if not protocol_graph: + print("Cannot draw graph: Graph object is empty.") + return + + G = nx.DiGraph() + + for node_id, attrs in protocol_graph.nodes.items(): + label = attrs.get("description", attrs.get("template", node_id[:8])) + G.add_node(node_id, label=label, **attrs) + + for edge in protocol_graph.edges: + G.add_edge(edge["source"], edge["target"]) + + plt.figure(figsize=(20, 15)) + try: + pos = nx.nx_agraph.graphviz_layout(G, prog="dot") + except Exception: + pos = nx.shell_layout(G) # Fallback layout + + node_labels = {node: data["label"] for node, data in G.nodes(data=True)} + nx.draw( + G, + pos, + with_labels=False, + node_size=2500, + node_color="skyblue", + node_shape="o", + edge_color="gray", + width=1.5, + arrowsize=15, + ) + nx.draw_networkx_labels(G, pos, labels=node_labels, font_size=8, font_weight="bold") + + plt.title("Chemical Protocol Workflow Graph", size=15) + plt.savefig(output_path, dpi=300, bbox_inches="tight") + plt.close() + print(f" - Visualization saved to '{output_path}'") + + +from networkx.drawing.nx_agraph import to_agraph +import re + +COMPASS = {"n","e","s","w","ne","nw","se","sw","c"} + +def _is_compass(port: str) -> bool: + return isinstance(port, str) and port.lower() in COMPASS + +def draw_protocol_graph_with_ports(protocol_graph, output_path: str, rankdir: str = "LR"): + """ + 使用 Graphviz 端口语法绘制协议工作流图。 + - 若边上的 source_port/target_port 是 compass(n/e/s/w/...),直接用 compass。 + - 否则自动为节点创建 record 形状并定义命名端口 。 + 最终由 PyGraphviz 渲染并输出到 output_path(后缀决定格式,如 .png/.svg/.pdf)。 + """ + if not protocol_graph: + print("Cannot draw graph: Graph object is empty.") + return + + # 1) 先用 networkx 搭建有向图,保留端口属性 + G = nx.DiGraph() + for node_id, attrs in protocol_graph.nodes.items(): + label = attrs.get("description", attrs.get("template", node_id[:8])) + # 保留一个干净的“中心标签”,用于放在 record 的中间槽 + G.add_node(node_id, _core_label=str(label), **{k:v for k,v in attrs.items() if k not in ("label",)}) + + edges_data = [] + in_ports_by_node = {} # 收集命名输入端口 + out_ports_by_node = {} # 收集命名输出端口 + + for edge in protocol_graph.edges: + u = edge["source"] + v = edge["target"] + sp = edge.get("source_port") + tp = edge.get("target_port") + + # 记录到图里(保留原始端口信息) + G.add_edge(u, v, source_port=sp, target_port=tp) + edges_data.append((u, v, sp, tp)) + + # 如果不是 compass,就按“命名端口”先归类,等会儿给节点造 record + if sp and not _is_compass(sp): + out_ports_by_node.setdefault(u, set()).add(str(sp)) + if tp and not _is_compass(tp): + in_ports_by_node.setdefault(v, set()).add(str(tp)) + + # 2) 转为 AGraph,使用 Graphviz 渲染 + A = to_agraph(G) + A.graph_attr.update(rankdir=rankdir, splines="true", concentrate="false", fontsize="10") + A.node_attr.update(shape="box", style="rounded,filled", fillcolor="lightyellow", color="#999999", fontname="Helvetica") + A.edge_attr.update(arrowsize="0.8", color="#666666") + + # 3) 为需要命名端口的节点设置 record 形状与 label + # 左列 = 输入端口;中间 = 核心标签;右列 = 输出端口 + for n in A.nodes(): + node = A.get_node(n) + core = G.nodes[n].get("_core_label", n) + + in_ports = sorted(in_ports_by_node.get(n, [])) + out_ports = sorted(out_ports_by_node.get(n, [])) + + # 如果该节点涉及命名端口,则用 record;否则保留原 box + if in_ports or out_ports: + def port_fields(ports): + if not ports: + return " " # 必须留一个空槽占位 + # 每个端口一个小格子,

name + return "|".join(f"<{re.sub(r'[^A-Za-z0-9_:.|-]', '_', p)}> {p}" for p in ports) + + left = port_fields(in_ports) + right = port_fields(out_ports) + + # 三栏:左(入) | 中(节点名) | 右(出) + record_label = f"{{ {left} | {core} | {right} }}" + node.attr.update(shape="record", label=record_label) + else: + # 没有命名端口:普通盒子,显示核心标签 + node.attr.update(label=str(core)) + + # 4) 给边设置 headport / tailport + # - 若端口为 compass:直接用 compass(e.g., headport="e") + # - 若端口为命名端口:使用在 record 中定义的 名(同名即可) + for (u, v, sp, tp) in edges_data: + e = A.get_edge(u, v) + + # Graphviz 属性:tail 是源,head 是目标 + if sp: + if _is_compass(sp): + e.attr["tailport"] = sp.lower() + else: + # 与 record label 中 名一致;特殊字符已在 label 中做了清洗 + e.attr["tailport"] = re.sub(r'[^A-Za-z0-9_:.|-]', '_', str(sp)) + + if tp: + if _is_compass(tp): + e.attr["headport"] = tp.lower() + else: + e.attr["headport"] = re.sub(r'[^A-Za-z0-9_:.|-]', '_', str(tp)) + + # 可选:若想让边更贴边缘,可设置 constraint/spline 等 + # e.attr["arrowhead"] = "vee" + + # 5) 输出 + A.draw(output_path, prog="dot") + print(f" - Port-aware workflow rendered to '{output_path}'") + + +def flatten_xdl_procedure(procedure_elem: ET.Element) -> List[ET.Element]: + """展平嵌套的XDL程序结构""" + flattened_operations = [] + TEMP_UNSUPPORTED_PROTOCOL = ["Purge", "Wait", "Stir", "ResetHandling"] + + def extract_operations(element: ET.Element): + if element.tag not in ["Prep", "Reaction", "Workup", "Purification", "Procedure"]: + if element.tag not in TEMP_UNSUPPORTED_PROTOCOL: + flattened_operations.append(element) + + for child in element: + extract_operations(child) + + for child in procedure_elem: + extract_operations(child) + + return flattened_operations + + +def parse_xdl_content(xdl_content: str) -> tuple: + """解析XDL内容""" + try: + xdl_content_cleaned = "".join(c for c in xdl_content if c.isprintable()) + root = ET.fromstring(xdl_content_cleaned) + + synthesis_elem = root.find("Synthesis") + if synthesis_elem is None: + return None, None, None + + # 解析硬件组件 + hardware_elem = synthesis_elem.find("Hardware") + hardware = [] + if hardware_elem is not None: + hardware = [{"id": c.get("id"), "type": c.get("type")} for c in hardware_elem.findall("Component")] + + # 解析试剂 + reagents_elem = synthesis_elem.find("Reagents") + reagents = [] + if reagents_elem is not None: + reagents = [{"name": r.get("name"), "role": r.get("role", "")} for r in reagents_elem.findall("Reagent")] + + # 解析程序 + procedure_elem = synthesis_elem.find("Procedure") + if procedure_elem is None: + return None, None, None + + flattened_operations = flatten_xdl_procedure(procedure_elem) + return hardware, reagents, flattened_operations + + except ET.ParseError as e: + raise ValueError(f"Invalid XDL format: {e}") + + +def convert_xdl_to_dict(xdl_content: str) -> Dict[str, Any]: + """ + 将XDL XML格式转换为标准的字典格式 + + Args: + xdl_content: XDL XML内容 + + Returns: + 转换结果,包含步骤和器材信息 + """ + try: + hardware, reagents, flattened_operations = parse_xdl_content(xdl_content) + if hardware is None: + return {"error": "Failed to parse XDL content", "success": False} + + # 将XDL元素转换为字典格式 + steps_data = [] + for elem in flattened_operations: + # 转换参数类型 + parameters = {} + for key, val in elem.attrib.items(): + converted_val = convert_to_type(val) + if converted_val is not None: + parameters[key] = converted_val + + step_dict = { + "operation": elem.tag, + "parameters": parameters, + "description": elem.get("purpose", f"Operation: {elem.tag}"), + } + steps_data.append(step_dict) + + # 合并硬件和试剂为统一的labware_info格式 + labware_data = [] + labware_data.extend({"id": hw["id"], "type": "hardware", **hw} for hw in hardware) + labware_data.extend({"name": reagent["name"], "type": "reagent", **reagent} for reagent in reagents) + + return { + "success": True, + "steps": steps_data, + "labware": labware_data, + "message": f"Successfully converted XDL to dict format. Found {len(steps_data)} steps and {len(labware_data)} labware items.", + } + + except Exception as e: + error_msg = f"XDL conversion failed: {str(e)}" + logger.error(error_msg) + return {"error": error_msg, "success": False} + + +def create_workflow( + steps_info: str, + labware_info: str, + workflow_name: str = "Generated Workflow", + workstation_name: str = "workstation", + workflow_description: str = "Auto-generated workflow from protocol", +) -> Dict[str, Any]: + """ + 创建工作流,输入数据已经是统一的字典格式 + + Args: + steps_info: 步骤信息 (JSON字符串,已经是list of dict格式) + labware_info: 实验器材和试剂信息 (JSON字符串,已经是list of dict格式) + workflow_name: 工作流名称 + workflow_description: 工作流描述 + + Returns: + 创建结果,包含工作流UUID和详细信息 + """ + try: + # 直接解析JSON数据 + steps_info_clean = extract_json_from_markdown(steps_info) + labware_info_clean = extract_json_from_markdown(labware_info) + + steps_data = json.loads(steps_info_clean) + labware_data = json.loads(labware_info_clean) + + # 统一处理所有数据 + protocol_graph = build_protocol_graph(labware_data, steps_data, workstation_name=workstation_name) + + # 检测协议类型(用于标签) + protocol_type = "bio" if any("biomek" in step.get("template", "") for step in refactored_steps) else "organic" + + # 转换为工作流格式 + data = protocol_graph.to_dict() + + # 转换节点格式 + for i, node in enumerate(data["nodes"]): + description = node.get("description", "") + onode = { + "template": node.pop("template"), + "id": node["id"], + "lab_node_type": node.get("lab_node_type", "Device"), + "name": description or f"Node {i + 1}", + "params": {"default": node}, + "handles": {}, + } + + # 处理边连接 + for edge in data["links"]: + if edge["source"] == node["id"]: + source_port = edge.get("source_port", "output") + if source_port not in onode["handles"]: + onode["handles"][source_port] = {"type": "source"} + + if edge["target"] == node["id"]: + target_port = edge.get("target_port", "input") + if target_port not in onode["handles"]: + onode["handles"][target_port] = {"type": "target"} + + data["nodes"][i] = onode + + # 发送到API创建工作流 + api_secret = configs.Lab.Key + if not api_secret: + return {"error": "API SecretKey is not configured", "success": False} + + # Step 1: 创建工作流 + workflow_url = f"{configs.Lab.Api}/api/v1/workflow/" + headers = { + "Content-Type": "application/json", + } + params = {"secret_key": api_secret} + + graph_data = {"name": workflow_name, **data} + + logger.info(f"Creating workflow: {workflow_name}") + response = requests.post( + workflow_url, params=params, json=graph_data, headers=headers, timeout=configs.Lab.Timeout + ) + response.raise_for_status() + + workflow_info = response.json() + + if workflow_info.get("code") != 0: + error_msg = f"API returned an error: {workflow_info.get('msg', 'Unknown Error')}" + logger.error(error_msg) + return {"error": error_msg, "success": False} + + workflow_uuid = workflow_info.get("data", {}).get("uuid") + if not workflow_uuid: + return {"error": "Failed to get workflow UUID from response", "success": False} + + # Step 2: 添加到模板库(可选) + try: + library_url = f"{configs.Lab.Api}/api/flociety/vs/workflows/library/" + lib_payload = { + "workflow_uuid": workflow_uuid, + "title": workflow_name, + "description": workflow_description, + "labels": [protocol_type.title(), "Auto-generated"], + } + + library_response = requests.post( + library_url, params=params, json=lib_payload, headers=headers, timeout=configs.Lab.Timeout + ) + library_response.raise_for_status() + + library_info = library_response.json() + logger.info(f"Workflow added to library: {library_info}") + + return { + "success": True, + "workflow_uuid": workflow_uuid, + "workflow_info": workflow_info.get("data"), + "library_info": library_info.get("data"), + "protocol_type": protocol_type, + "message": f"Workflow '{workflow_name}' created successfully", + } + + except Exception as e: + # 即使添加到库失败,工作流创建仍然成功 + logger.warning(f"Failed to add workflow to library: {str(e)}") + return { + "success": True, + "workflow_uuid": workflow_uuid, + "workflow_info": workflow_info.get("data"), + "protocol_type": protocol_type, + "message": f"Workflow '{workflow_name}' created successfully (library addition failed)", + } + + except requests.exceptions.RequestException as e: + error_msg = f"Network error when calling API: {str(e)}" + logger.error(error_msg) + return {"error": error_msg, "success": False} + except json.JSONDecodeError as e: + error_msg = f"JSON parsing error: {str(e)}" + logger.error(error_msg) + return {"error": error_msg, "success": False} + except Exception as e: + error_msg = f"An unexpected error occurred: {str(e)}" + logger.error(error_msg) + logger.error(traceback.format_exc()) + return {"error": error_msg, "success": False} diff --git a/test/experiments/dispensing_station_bioyond.json b/test/experiments/dispensing_station_bioyond.json index 745e1289..751eac09 100644 --- a/test/experiments/dispensing_station_bioyond.json +++ b/test/experiments/dispensing_station_bioyond.json @@ -8,7 +8,7 @@ ], "parent": null, "type": "device", - "class": "workstation.bioyond_dispensing_station", + "class": "bioyond_dispensing_station", "config": { "config": { "api_key": "DE9BDDA0", diff --git a/test/workflow/example_bio.json b/test/workflow/example_bio.json new file mode 100644 index 00000000..d0f0d7a3 --- /dev/null +++ b/test/workflow/example_bio.json @@ -0,0 +1,186 @@ +{ + "workflow": [ + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_1", + "targets": "Liquid_2", + "asp_vol": 66.0, + "dis_vol": 66.0, + "asp_flow_rate": 94.0, + "dis_flow_rate": 94.0 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_2", + "targets": "Liquid_3", + "asp_vol": 58.0, + "dis_vol": 96.0, + "asp_flow_rate": 94.0, + "dis_flow_rate": 94.0 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_4", + "targets": "Liquid_2", + "asp_vol": 85.0, + "dis_vol": 170.0, + "asp_flow_rate": 94.0, + "dis_flow_rate": 94.0 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_4", + "targets": "Liquid_2", + "asp_vol": 63.333333333333336, + "dis_vol": 170.0, + "asp_flow_rate": 94.0, + "dis_flow_rate": 94.0 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_2", + "targets": "Liquid_3", + "asp_vol": 72.0, + "dis_vol": 150.0, + "asp_flow_rate": 94.0, + "dis_flow_rate": 94.0 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_4", + "targets": "Liquid_2", + "asp_vol": 85.0, + "dis_vol": 170.0, + "asp_flow_rate": 94.0, + "dis_flow_rate": 94.0 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_4", + "targets": "Liquid_2", + "asp_vol": 63.333333333333336, + "dis_vol": 170.0, + "asp_flow_rate": 94.0, + "dis_flow_rate": 94.0 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_2", + "targets": "Liquid_3", + "asp_vol": 72.0, + "dis_vol": 150.0, + "asp_flow_rate": 94.0, + "dis_flow_rate": 94.0 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_2", + "targets": "Liquid_3", + "asp_vol": 20.0, + "dis_vol": 20.0, + "asp_flow_rate": 7.6, + "dis_flow_rate": 7.6 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_5", + "targets": "Liquid_2", + "asp_vol": 6.0, + "dis_vol": 12.0, + "asp_flow_rate": 7.6, + "dis_flow_rate": 7.6 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_5", + "targets": "Liquid_2", + "asp_vol": 10.666666666666666, + "dis_vol": 12.0, + "asp_flow_rate": 7.599999999999999, + "dis_flow_rate": 7.6 + } + }, + { + "action": "transfer_liquid", + "action_args": { + "sources": "Liquid_2", + "targets": "Liquid_6", + "asp_vol": 12.0, + "dis_vol": 10.0, + "asp_flow_rate": 7.6, + "dis_flow_rate": 7.6 + } + } + ], + "reagent": { + "Liquid_6": { + "slot": 1, + "well": [ + "A2" + ], + "labware": "elution plate" + }, + "Liquid_1": { + "slot": 2, + "well": [ + "A1", + "A2", + "A4" + ], + "labware": "reagent reservoir" + }, + "Liquid_4": { + "slot": 2, + "well": [ + "A1", + "A2", + "A4" + ], + "labware": "reagent reservoir" + }, + "Liquid_5": { + "slot": 2, + "well": [ + "A1", + "A2", + "A4" + ], + "labware": "reagent reservoir" + }, + "Liquid_2": { + "slot": 4, + "well": [ + "A2" + ], + "labware": "TAG1 plate on Magnetic Module GEN2" + }, + "Liquid_3": { + "slot": 12, + "well": [ + "A1" + ], + "labware": "Opentrons Fixed Trash" + } + } +} \ No newline at end of file diff --git a/test/workflow/example_bio_graph.png b/test/workflow/example_bio_graph.png new file mode 100644 index 00000000..351cceb8 Binary files /dev/null and b/test/workflow/example_bio_graph.png differ diff --git a/test/workflow/example_prcxi.json b/test/workflow/example_prcxi.json new file mode 100644 index 00000000..d9abd3d8 --- /dev/null +++ b/test/workflow/example_prcxi.json @@ -0,0 +1,63 @@ +{ + "steps_info": [ + { + "step_number": 1, + "action": "transfer_liquid", + "parameters": { + "source": "sample supernatant", + "target": "antibody-coated well", + "volume": 100 + } + }, + { + "step_number": 2, + "action": "transfer_liquid", + "parameters": { + "source": "washing buffer", + "target": "antibody-coated well", + "volume": 200 + } + }, + { + "step_number": 3, + "action": "transfer_liquid", + "parameters": { + "source": "washing buffer", + "target": "antibody-coated well", + "volume": 200 + } + }, + { + "step_number": 4, + "action": "transfer_liquid", + "parameters": { + "source": "washing buffer", + "target": "antibody-coated well", + "volume": 200 + } + }, + { + "step_number": 5, + "action": "transfer_liquid", + "parameters": { + "source": "TMB substrate", + "target": "antibody-coated well", + "volume": 100 + } + } + ], + "labware_info": [ + {"reagent_name": "sample supernatant", "material_name": "96深孔板", "positions": 1}, + {"reagent_name": "washing buffer", "material_name": "储液槽", "positions": 2}, + {"reagent_name": "TMB substrate", "material_name": "储液槽", "positions": 3}, + {"reagent_name": "antibody-coated well", "material_name": "96 细胞培养皿", "positions": 4}, + {"reagent_name": "", "material_name": "300μL Tip头", "positions": 5}, + {"reagent_name": "", "material_name": "300μL Tip头", "positions": 6}, + {"reagent_name": "", "material_name": "300μL Tip头", "positions": 7}, + {"reagent_name": "", "material_name": "300μL Tip头", "positions": 8}, + {"reagent_name": "", "material_name": "300μL Tip头", "positions": 9}, + {"reagent_name": "", "material_name": "300μL Tip头", "positions": 10}, + {"reagent_name": "", "material_name": "300μL Tip头", "positions": 11}, + {"reagent_name": "", "material_name": "300μL Tip头", "positions": 13} + ] +} \ No newline at end of file diff --git a/test/workflow/example_prcxi_graph.png b/test/workflow/example_prcxi_graph.png new file mode 100644 index 00000000..96cecdb5 Binary files /dev/null and b/test/workflow/example_prcxi_graph.png differ diff --git a/test/workflow/example_prcxi_graph_20251022_1359.png b/test/workflow/example_prcxi_graph_20251022_1359.png new file mode 100644 index 00000000..7cf4f7e7 Binary files /dev/null and b/test/workflow/example_prcxi_graph_20251022_1359.png differ diff --git a/test/workflow/merge_workflow.py b/test/workflow/merge_workflow.py new file mode 100644 index 00000000..fb409769 --- /dev/null +++ b/test/workflow/merge_workflow.py @@ -0,0 +1,94 @@ +import json +import sys +from datetime import datetime +from pathlib import Path + +ROOT_DIR = Path(__file__).resolve().parents[2] +if str(ROOT_DIR) not in sys.path: + sys.path.insert(0, str(ROOT_DIR)) + +import pytest + +from scripts.workflow import build_protocol_graph, draw_protocol_graph, draw_protocol_graph_with_ports + + +ROOT_DIR = Path(__file__).resolve().parents[2] +if str(ROOT_DIR) not in sys.path: + sys.path.insert(0, str(ROOT_DIR)) + + +def _normalize_steps(data): + normalized = [] + for step in data: + action = step.get("action") or step.get("operation") + if not action: + continue + raw_params = step.get("parameters") or step.get("action_args") or {} + params = dict(raw_params) + + if "source" in raw_params and "sources" not in raw_params: + params["sources"] = raw_params["source"] + if "target" in raw_params and "targets" not in raw_params: + params["targets"] = raw_params["target"] + + description = step.get("description") or step.get("purpose") + step_dict = {"action": action, "parameters": params} + if description: + step_dict["description"] = description + normalized.append(step_dict) + return normalized + + +def _normalize_labware(data): + labware = {} + for item in data: + reagent_name = item.get("reagent_name") + key = reagent_name or item.get("material_name") or item.get("name") + if not key: + continue + key = str(key) + idx = 1 + original_key = key + while key in labware: + idx += 1 + key = f"{original_key}_{idx}" + + labware[key] = { + "slot": item.get("positions") or item.get("slot"), + "labware": item.get("material_name") or item.get("labware"), + "well": item.get("well", []), + "type": item.get("type", "reagent"), + "role": item.get("role", ""), + "name": key, + } + return labware + + +@pytest.mark.parametrize("protocol_name", [ + "example_bio", + # "bioyond_materials_liquidhandling_1", + "example_prcxi", +]) +def test_build_protocol_graph(protocol_name): + data_path = Path(__file__).with_name(f"{protocol_name}.json") + with data_path.open("r", encoding="utf-8") as fp: + d = json.load(fp) + + if "workflow" in d and "reagent" in d: + protocol_steps = d["workflow"] + labware_info = d["reagent"] + elif "steps_info" in d and "labware_info" in d: + protocol_steps = _normalize_steps(d["steps_info"]) + labware_info = _normalize_labware(d["labware_info"]) + else: + raise ValueError("Unsupported protocol format") + + graph = build_protocol_graph( + labware_info=labware_info, + protocol_steps=protocol_steps, + workstation_name="PRCXi", + ) + timestamp = datetime.now().strftime("%Y%m%d_%H%M") + output_path = data_path.with_name(f"{protocol_name}_graph_{timestamp}.png") + draw_protocol_graph_with_ports(graph, str(output_path)) + print(graph) \ No newline at end of file diff --git a/unilabos/app/web/client.py b/unilabos/app/web/client.py index b8c8bea3..72c079a1 100644 --- a/unilabos/app/web/client.py +++ b/unilabos/app/web/client.py @@ -6,6 +6,8 @@ HTTP客户端模块 import json import os +import time +from threading import Thread from typing import List, Dict, Any, Optional import requests @@ -84,14 +86,14 @@ class HTTPClient: f"{self.remote_addr}/edge/material", json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid}, headers={"Authorization": f"Lab {self.auth}"}, - timeout=100, + timeout=60, ) else: response = requests.put( f"{self.remote_addr}/edge/material", json={"nodes": [x for xs in resources.dump() for x in xs], "mount_uuid": mount_uuid}, headers={"Authorization": f"Lab {self.auth}"}, - timeout=100, + timeout=10, ) with open(os.path.join(BasicConfig.working_dir, "res_resource_tree_add.json"), "w", encoding="utf-8") as f: @@ -126,12 +128,16 @@ class HTTPClient: Returns: Dict[str, str]: 旧UUID到新UUID的映射关系 {old_uuid: new_uuid} """ + with open(os.path.join(BasicConfig.working_dir, "req_resource_tree_get.json"), "w", encoding="utf-8") as f: + f.write(json.dumps({"uuids": uuid_list, "with_children": with_children}, indent=4)) response = requests.post( f"{self.remote_addr}/edge/material/query", json={"uuids": uuid_list, "with_children": with_children}, headers={"Authorization": f"Lab {self.auth}"}, timeout=100, ) + with open(os.path.join(BasicConfig.working_dir, "res_resource_tree_get.json"), "w", encoding="utf-8") as f: + f.write(f"{response.status_code}" + "\n" + response.text) if response.status_code == 200: res = response.json() if "code" in res and res["code"] != 0: @@ -187,12 +193,16 @@ class HTTPClient: Returns: Dict: 返回的资源数据 """ + with open(os.path.join(BasicConfig.working_dir, "req_resource_get.json"), "w", encoding="utf-8") as f: + f.write(json.dumps({"id": id, "with_children": with_children}, indent=4)) response = requests.get( f"{self.remote_addr}/lab/material", params={"id": id, "with_children": with_children}, headers={"Authorization": f"Lab {self.auth}"}, timeout=20, ) + with open(os.path.join(BasicConfig.working_dir, "res_resource_get.json"), "w", encoding="utf-8") as f: + f.write(f"{response.status_code}" + "\n" + response.text) return response.json() def resource_del(self, id: str) -> requests.Response: diff --git a/unilabos/devices/workstation/bioyond_studio/config.py b/unilabos/devices/workstation/bioyond_studio/config.py index 566d30d6..504cf459 100644 --- a/unilabos/devices/workstation/bioyond_studio/config.py +++ b/unilabos/devices/workstation/bioyond_studio/config.py @@ -16,7 +16,7 @@ API_CONFIG = { "report_token": os.getenv("BIOYOND_REPORT_TOKEN", "CHANGE_ME_TOKEN"), # HTTP 服务配置 - "HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.21.32.91"), # HTTP服务监听地址,监听计算机飞连ip地址 + "HTTP_host": os.getenv("BIOYOND_HTTP_HOST", "172.21.32.210"), # HTTP服务监听地址,监听计算机飞连ip地址 "HTTP_port": int(os.getenv("BIOYOND_HTTP_PORT", "8080")), "debug_mode": False,# 调试模式 } diff --git a/unilabos/devices/workstation/bioyond_studio/dispensing_station.py b/unilabos/devices/workstation/bioyond_studio/dispensing_station.py index 11b011cc..8617a13f 100644 --- a/unilabos/devices/workstation/bioyond_studio/dispensing_station.py +++ b/unilabos/devices/workstation/bioyond_studio/dispensing_station.py @@ -7,7 +7,7 @@ from unilabos.devices.workstation.bioyond_studio.station import BioyondWorkstati class BioyondDispensingStation(BioyondWorkstation): def __init__( - self, + self, config, # 桌子 deck, @@ -77,7 +77,7 @@ class BioyondDispensingStation(BioyondWorkstation): - hold_m_name: 库位名称,如"C01",用于查找对应的holdMId 返回: 任务创建结果 - + 异常: - BioyondException: 各种错误情况下的统一异常 """ @@ -85,7 +85,7 @@ class BioyondDispensingStation(BioyondWorkstation): # 1. 参数验证 if not hold_m_name: raise BioyondException("hold_m_name 是必填参数") - + # 检查90%物料参数的完整性 # 90%_1物料:如果有物料名称或目标重量,就必须有全部参数 if percent_90_1_assign_material_name or percent_90_1_target_weigh: @@ -93,21 +93,21 @@ class BioyondDispensingStation(BioyondWorkstation): raise BioyondException("90%_1物料:如果提供了目标重量,必须同时提供物料名称") if not percent_90_1_target_weigh: raise BioyondException("90%_1物料:如果提供了物料名称,必须同时提供目标重量") - + # 90%_2物料:如果有物料名称或目标重量,就必须有全部参数 if percent_90_2_assign_material_name or percent_90_2_target_weigh: if not percent_90_2_assign_material_name: raise BioyondException("90%_2物料:如果提供了目标重量,必须同时提供物料名称") if not percent_90_2_target_weigh: raise BioyondException("90%_2物料:如果提供了物料名称,必须同时提供目标重量") - + # 90%_3物料:如果有物料名称或目标重量,就必须有全部参数 if percent_90_3_assign_material_name or percent_90_3_target_weigh: if not percent_90_3_assign_material_name: raise BioyondException("90%_3物料:如果提供了目标重量,必须同时提供物料名称") if not percent_90_3_target_weigh: raise BioyondException("90%_3物料:如果提供了物料名称,必须同时提供目标重量") - + # 检查10%物料参数的完整性 # 10%_1物料:如果有物料名称、目标重量、体积或液体物料名称中的任何一个,就必须有全部参数 if any([percent_10_1_assign_material_name, percent_10_1_target_weigh, percent_10_1_volume, percent_10_1_liquid_material_name]): @@ -119,7 +119,7 @@ class BioyondDispensingStation(BioyondWorkstation): raise BioyondException("10%_1物料:如果提供了其他参数,必须同时提供液体体积") if not percent_10_1_liquid_material_name: raise BioyondException("10%_1物料:如果提供了其他参数,必须同时提供液体物料名称") - + # 10%_2物料:如果有物料名称、目标重量、体积或液体物料名称中的任何一个,就必须有全部参数 if any([percent_10_2_assign_material_name, percent_10_2_target_weigh, percent_10_2_volume, percent_10_2_liquid_material_name]): if not percent_10_2_assign_material_name: @@ -130,7 +130,7 @@ class BioyondDispensingStation(BioyondWorkstation): raise BioyondException("10%_2物料:如果提供了其他参数,必须同时提供液体体积") if not percent_10_2_liquid_material_name: raise BioyondException("10%_2物料:如果提供了其他参数,必须同时提供液体物料名称") - + # 10%_3物料:如果有物料名称、目标重量、体积或液体物料名称中的任何一个,就必须有全部参数 if any([percent_10_3_assign_material_name, percent_10_3_target_weigh, percent_10_3_volume, percent_10_3_liquid_material_name]): if not percent_10_3_assign_material_name: @@ -141,7 +141,7 @@ class BioyondDispensingStation(BioyondWorkstation): raise BioyondException("10%_3物料:如果提供了其他参数,必须同时提供液体体积") if not percent_10_3_liquid_material_name: raise BioyondException("10%_3物料:如果提供了其他参数,必须同时提供液体物料名称") - + # 2. 生成任务编码和设置默认值 order_code = "task_vial_" + str(int(datetime.now().timestamp())) if order_name is None: @@ -152,7 +152,7 @@ class BioyondDispensingStation(BioyondWorkstation): temperature = "40" if delay_time is None: delay_time = "600" - + # 3. 工作流ID workflow_id = "3a19310d-16b9-9d81-b109-0748e953694b" @@ -160,22 +160,22 @@ class BioyondDispensingStation(BioyondWorkstation): material_info = self.hardware_interface.material_id_query(workflow_id) if not material_info: raise BioyondException(f"无法查询工作流 {workflow_id} 的物料信息") - + # 获取locations列表 locations = material_info.get("locations", []) if isinstance(material_info, dict) else [] if not locations: raise BioyondException(f"工作流 {workflow_id} 没有找到库位信息") - + # 查找指定名称的库位 hold_mid = None for location in locations: if location.get("holdMName") == hold_m_name: hold_mid = location.get("holdMId") break - + if not hold_mid: raise BioyondException(f"未找到库位名称为 {hold_m_name} 的库位,请检查名称是否正确") - + extend_properties = f"{{\"{ hold_mid }\": {{}}}}" self.hardware_interface._logger.info(f"找到库位 {hold_m_name} 对应的holdMId: {hold_mid}") @@ -271,7 +271,7 @@ class BioyondDispensingStation(BioyondWorkstation): result = self.hardware_interface.create_order(json_str) self.hardware_interface._logger.info(f"创建90%10%小瓶投料任务结果: {result}") return json.dumps({"suc": True}) - + except BioyondException: # 重新抛出BioyondException raise @@ -307,7 +307,7 @@ class BioyondDispensingStation(BioyondWorkstation): - hold_m_name: 库位名称,如"ODA-1",用于查找对应的holdMId 返回: 任务创建结果 - + 异常: - BioyondException: 各种错误情况下的统一异常 """ @@ -321,8 +321,8 @@ class BioyondDispensingStation(BioyondWorkstation): raise BioyondException("volume 是必填参数") if not hold_m_name: raise BioyondException("hold_m_name 是必填参数") - - + + # 2. 生成任务编码和设置默认值 order_code = "task_oda_" + str(int(datetime.now().timestamp())) if order_name is None: @@ -333,30 +333,30 @@ class BioyondDispensingStation(BioyondWorkstation): temperature = "20" if delay_time is None: delay_time = "600" - + # 3. 工作流ID - 二胺溶液配置工作流 workflow_id = "3a15d4a1-3bbe-76f9-a458-292896a338f5" - + # 4. 查询工作流对应的holdMID - material_info = self.material_id_query(workflow_id) + material_info = self.hardware_interface.material_id_query(workflow_id) if not material_info: raise BioyondException(f"无法查询工作流 {workflow_id} 的物料信息") - + # 获取locations列表 locations = material_info.get("locations", []) if isinstance(material_info, dict) else [] if not locations: raise BioyondException(f"工作流 {workflow_id} 没有找到库位信息") - + # 查找指定名称的库位 hold_mid = None for location in locations: if location.get("holdMName") == hold_m_name: hold_mid = location.get("holdMId") break - + if not hold_mid: raise BioyondException(f"未找到库位名称为 {hold_m_name} 的库位,请检查名称是否正确") - + extend_properties = f"{{\"{ hold_mid }\": {{}}}}" self.hardware_interface._logger.info(f"找到库位 {hold_m_name} 对应的holdMId: {hold_mid}") @@ -397,9 +397,9 @@ class BioyondDispensingStation(BioyondWorkstation): # 7. 调用create_order方法创建任务 result = self.hardware_interface.create_order(json_str) self.hardware_interface._logger.info(f"创建二胺溶液配置任务结果: {result}") - + return json.dumps({"suc": True}) - + except BioyondException: # 重新抛出BioyondException raise @@ -409,17 +409,278 @@ class BioyondDispensingStation(BioyondWorkstation): self.hardware_interface._logger.error(error_msg) raise BioyondException(error_msg) + # 批量创建二胺溶液配置任务 + def batch_create_diamine_solution_tasks(self, + solutions, + liquid_material_name: str = "NMP", + speed: str = None, + temperature: str = None, + delay_time: str = None) -> str: + """ + 批量创建二胺溶液配置任务 + + 参数说明: + - solutions: 溶液列表(数组)或JSON字符串,格式如下: + [ + { + "name": "MDA", + "order": 0, + "solid_mass": 5.0, + "solvent_volume": 20, + ... + }, + ... + ] + - liquid_material_name: 液体物料名称,默认为"NMP" + - speed: 搅拌速度,如果为None则使用默认值400 + - temperature: 温度,如果为None则使用默认值20 + - delay_time: 延迟时间,如果为None则使用默认值600 + + 返回: JSON字符串格式的任务创建结果 + + 异常: + - BioyondException: 各种错误情况下的统一异常 + """ + try: + # 参数类型转换:如果是字符串则解析为列表 + if isinstance(solutions, str): + try: + solutions = json.loads(solutions) + except json.JSONDecodeError as e: + raise BioyondException(f"solutions JSON解析失败: {str(e)}") + + # 参数验证 + if not isinstance(solutions, list): + raise BioyondException("solutions 必须是列表类型或有效的JSON数组字符串") + + if not solutions: + raise BioyondException("solutions 列表不能为空") + + # 批量创建任务 + results = [] + success_count = 0 + failed_count = 0 + + for idx, solution in enumerate(solutions): + try: + # 提取参数 + name = solution.get("name") + solid_mass = solution.get("solid_mass") + solvent_volume = solution.get("solvent_volume") + order = solution.get("order") + + if not all([name, solid_mass is not None, solvent_volume is not None]): + self.hardware_interface._logger.warning( + f"跳过第 {idx + 1} 个溶液:缺少必要参数" + ) + results.append({ + "index": idx + 1, + "name": name, + "success": False, + "error": "缺少必要参数" + }) + failed_count += 1 + continue + + # 生成库位名称(直接使用物料名称) + # 如果需要其他命名规则,可以在这里调整 + hold_m_name = name + + # 调用单个任务创建方法 + result = self.create_diamine_solution_task( + order_name=f"二胺溶液配置-{name}", + material_name=name, + target_weigh=str(solid_mass), + volume=str(solvent_volume), + liquid_material_name=liquid_material_name, + speed=speed, + temperature=temperature, + delay_time=delay_time, + hold_m_name=hold_m_name + ) + + results.append({ + "index": idx + 1, + "name": name, + "success": True, + "hold_m_name": hold_m_name + }) + success_count += 1 + self.hardware_interface._logger.info( + f"成功创建二胺溶液配置任务: {name}" + ) + + except BioyondException as e: + results.append({ + "index": idx + 1, + "name": solution.get("name", "unknown"), + "success": False, + "error": str(e) + }) + failed_count += 1 + self.hardware_interface._logger.error( + f"创建第 {idx + 1} 个任务失败: {str(e)}" + ) + except Exception as e: + results.append({ + "index": idx + 1, + "name": solution.get("name", "unknown"), + "success": False, + "error": f"未知错误: {str(e)}" + }) + failed_count += 1 + self.hardware_interface._logger.error( + f"创建第 {idx + 1} 个任务时发生未知错误: {str(e)}" + ) + + # 返回汇总结果 + summary = { + "total": len(solutions), + "success": success_count, + "failed": failed_count, + "details": results + } + + self.hardware_interface._logger.info( + f"批量创建二胺溶液配置任务完成: 总数={len(solutions)}, " + f"成功={success_count}, 失败={failed_count}" + ) + + # 返回JSON字符串格式 + return json.dumps(summary, ensure_ascii=False) + + except BioyondException: + raise + except Exception as e: + error_msg = f"批量创建二胺溶液配置任务时发生未预期的错误: {str(e)}" + self.hardware_interface._logger.error(error_msg) + raise BioyondException(error_msg) + + # 批量创建90%10%小瓶投料任务 + def batch_create_90_10_vial_feeding_tasks(self, + titration, + hold_m_name: str = None, + speed: str = None, + temperature: str = None, + delay_time: str = None, + liquid_material_name: str = "NMP") -> str: + """ + 批量创建90%10%小瓶投料任务(仅创建1个任务,但包含所有90%和10%物料) + + 参数说明: + - titration: 滴定信息的字典或JSON字符串,格式如下: + { + "name": "BTDA", + "main_portion": 1.9152351915461294, # 主称固体质量(g) -> 90%物料 + "titration_portion": 0.05923407808905555, # 滴定固体质量(g) -> 10%物料固体 + "titration_solvent": 3.050555021586361 # 滴定溶液体积(mL) -> 10%物料液体 + } + - hold_m_name: 库位名称,如"C01"。必填参数 + - speed: 搅拌速度,如果为None则使用默认值400 + - temperature: 温度,如果为None则使用默认值40 + - delay_time: 延迟时间,如果为None则使用默认值600 + - liquid_material_name: 10%物料的液体物料名称,默认为"NMP" + + 返回: JSON字符串格式的任务创建结果 + + 异常: + - BioyondException: 各种错误情况下的统一异常 + """ + try: + # 参数类型转换:如果是字符串则解析为字典 + if isinstance(titration, str): + try: + titration = json.loads(titration) + except json.JSONDecodeError as e: + raise BioyondException(f"titration参数JSON解析失败: {str(e)}") + + # 参数验证 + if not isinstance(titration, dict): + raise BioyondException("titration 必须是字典类型或有效的JSON字符串") + + if not hold_m_name: + raise BioyondException("hold_m_name 是必填参数") + + if not titration: + raise BioyondException("titration 参数不能为空") + + # 提取滴定数据 + name = titration.get("name") + main_portion = titration.get("main_portion") # 主称固体质量 + titration_portion = titration.get("titration_portion") # 滴定固体质量 + titration_solvent = titration.get("titration_solvent") # 滴定溶液体积 + + if not all([name, main_portion is not None, titration_portion is not None, titration_solvent is not None]): + raise BioyondException("titration 数据缺少必要参数") + + # 将main_portion平均分成3份作为90%物料(3个小瓶) + portion_90 = main_portion / 3 + + # 调用单个任务创建方法 + result = self.create_90_10_vial_feeding_task( + order_name=f"90%10%小瓶投料-{name}", + speed=speed, + temperature=temperature, + delay_time=delay_time, + # 90%物料 - 主称固体平均分成3份 + percent_90_1_assign_material_name=name, + percent_90_1_target_weigh=str(round(portion_90, 6)), + percent_90_2_assign_material_name=name, + percent_90_2_target_weigh=str(round(portion_90, 6)), + percent_90_3_assign_material_name=name, + percent_90_3_target_weigh=str(round(portion_90, 6)), + # 10%物料 - 滴定固体 + 滴定溶剂(只使用第1个10%小瓶) + percent_10_1_assign_material_name=name, + percent_10_1_target_weigh=str(round(titration_portion, 6)), + percent_10_1_volume=str(round(titration_solvent, 6)), + percent_10_1_liquid_material_name=liquid_material_name, + hold_m_name=hold_m_name + ) + + summary = { + "success": True, + "hold_m_name": hold_m_name, + "material_name": name, + "90_vials": { + "count": 3, + "weight_per_vial": round(portion_90, 6), + "total_weight": round(main_portion, 6) + }, + "10_vials": { + "count": 1, + "solid_weight": round(titration_portion, 6), + "liquid_volume": round(titration_solvent, 6) + } + } + + self.hardware_interface._logger.info( + f"成功创建90%10%小瓶投料任务: {hold_m_name}, " + f"90%物料={portion_90:.6f}g×3, 10%物料={titration_portion:.6f}g+{titration_solvent:.6f}mL" + ) + + # 返回JSON字符串格式 + return json.dumps(summary, ensure_ascii=False) + + except BioyondException: + raise + except Exception as e: + error_msg = f"批量创建90%10%小瓶投料任务时发生未预期的错误: {str(e)}" + self.hardware_interface._logger.error(error_msg) + raise BioyondException(error_msg) + if __name__ == "__main__": bioyond = BioyondDispensingStation(config={ "api_key": "DE9BDDA0", "api_host": "http://192.168.1.200:44388" }) - + + # ============ 原有示例代码 ============ + # 示例1:使用material_id_query查询工作流对应的holdMID workflow_id_1 = "3a15d4a1-3bbe-76f9-a458-292896a338f5" # 二胺溶液配置工作流ID workflow_id_2 = "3a19310d-16b9-9d81-b109-0748e953694b" # 90%10%小瓶投料工作流ID - + #示例2:创建二胺溶液配置任务 - ODA,指定库位名称 # bioyond.create_diamine_solution_task( # order_code="task_oda_" + str(int(datetime.now().timestamp())), @@ -433,7 +694,7 @@ if __name__ == "__main__": # delay_time="600", # hold_m_name="烧杯ODA" # ) - + # bioyond.create_diamine_solution_task( # order_code="task_pda_" + str(int(datetime.now().timestamp())), # order_name="二胺溶液配置-PDA", @@ -446,7 +707,7 @@ if __name__ == "__main__": # delay_time="600", # hold_m_name="烧杯PDA-2" # ) - + # bioyond.create_diamine_solution_task( # order_code="task_mpda_" + str(int(datetime.now().timestamp())), # order_name="二胺溶液配置-MPDA", @@ -462,8 +723,8 @@ if __name__ == "__main__": bioyond.material_id_query("3a19310d-16b9-9d81-b109-0748e953694b") bioyond.material_id_query("3a15d4a1-3bbe-76f9-a458-292896a338f5") - - + + #示例4:创建90%10%小瓶投料任务 # vial_result = bioyond.create_90_10_vial_feeding_task( # order_code="task_vial_" + str(int(datetime.now().timestamp())), @@ -487,7 +748,7 @@ if __name__ == "__main__": # delay_time="1200", # hold_m_name="8.4分装板-1" # ) - + # vial_result = bioyond.create_90_10_vial_feeding_task( # order_code="task_vial_" + str(int(datetime.now().timestamp())), # order_name="90%10%小瓶投料-2", @@ -510,7 +771,7 @@ if __name__ == "__main__": # delay_time="1200", # hold_m_name="8.4分装板-2" # ) - + #启动调度器 #bioyond.scheduler_start() @@ -529,7 +790,7 @@ if __name__ == "__main__": material_data_yp = { "typeId": "3a14196e-b7a0-a5da-1931-35f3000281e9", #"code": "物料编码001", - #"barCode": "物料条码001", + #"barCode": "物料条码001", "name": "8.4样品板", "unit": "个", "quantity": 1, @@ -540,7 +801,7 @@ if __name__ == "__main__": "name": "BTDA-1", "quantity": 20, "x": 1, - "y": 1, + "y": 1, #"unit": "单位" "molecular": 1, "Parameters":"{\"molecular\": 1}" @@ -585,7 +846,7 @@ if __name__ == "__main__": material_data_yp = { "typeId": "3a14196e-b7a0-a5da-1931-35f3000281e9", #"code": "物料编码001", - #"barCode": "物料条码001", + #"barCode": "物料条码001", "name": "8.7样品板", "unit": "个", "quantity": 1, @@ -596,7 +857,7 @@ if __name__ == "__main__": "name": "mianfen", "quantity": 13, "x": 1, - "y": 1, + "y": 1, #"unit": "单位" "molecular": 1, "Parameters":"{\"molecular\": 1}" @@ -620,7 +881,7 @@ if __name__ == "__main__": material_data_fzb_1 = { "typeId": "3a14196e-5dfe-6e21-0c79-fe2036d052c4", #"code": "物料编码001", - #"barCode": "物料条码001", + #"barCode": "物料条码001", "name": "8.7分装板", "unit": "个", "quantity": 1, @@ -631,7 +892,7 @@ if __name__ == "__main__": "name": "10%小瓶1", "quantity": 1, "x": 1, - "y": 1, + "y": 1, #"unit": "单位" "molecular": 1, "Parameters":"{\"molecular\": 1}" @@ -642,7 +903,7 @@ if __name__ == "__main__": "name": "10%小瓶2", "quantity": 1, "x": 1, - "y": 2, + "y": 2, #"unit": "单位" "molecular": 1, "Parameters":"{\"molecular\": 1}" @@ -653,7 +914,7 @@ if __name__ == "__main__": "name": "10%小瓶3", "quantity": 1, "x": 1, - "y": 3, + "y": 3, #"unit": "单位" "molecular": 1, "Parameters":"{\"molecular\": 1}" @@ -697,7 +958,7 @@ if __name__ == "__main__": material_data_fzb_2 = { "typeId": "3a14196e-5dfe-6e21-0c79-fe2036d052c4", #"code": "物料编码001", - #"barCode": "物料条码001", + #"barCode": "物料条码001", "name": "8.4分装板-2", "unit": "个", "quantity": 1, @@ -708,7 +969,7 @@ if __name__ == "__main__": "name": "10%小瓶1", "quantity": 1, "x": 1, - "y": 1, + "y": 1, #"unit": "单位" "molecular": 1, "Parameters":"{\"molecular\": 1}" @@ -719,7 +980,7 @@ if __name__ == "__main__": "name": "10%小瓶2", "quantity": 1, "x": 1, - "y": 2, + "y": 2, #"unit": "单位" "molecular": 1, "Parameters":"{\"molecular\": 1}" @@ -730,7 +991,7 @@ if __name__ == "__main__": "name": "10%小瓶3", "quantity": 1, "x": 1, - "y": 3, + "y": 3, #"unit": "单位" "molecular": 1, "Parameters":"{\"molecular\": 1}" @@ -775,7 +1036,7 @@ if __name__ == "__main__": material_data_sb_oda = { "typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a", #"code": "物料编码001", - #"barCode": "物料条码001", + #"barCode": "物料条码001", "name": "mianfen1", "unit": "个", "quantity": 1, @@ -785,7 +1046,7 @@ if __name__ == "__main__": material_data_sb_pda_2 = { "typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a", #"code": "物料编码001", - #"barCode": "物料条码001", + #"barCode": "物料条码001", "name": "mianfen2", "unit": "个", "quantity": 1, @@ -795,7 +1056,7 @@ if __name__ == "__main__": # material_data_sb_mpda = { # "typeId": "3a14196b-24f2-ca49-9081-0cab8021bf1a", # #"code": "物料编码001", - # #"barCode": "物料条码001", + # #"barCode": "物料条码001", # "name": "烧杯MPDA", # "unit": "个", # "quantity": 1, diff --git a/unilabos/devices/workstation/bioyond_studio/reaction_station.py b/unilabos/devices/workstation/bioyond_studio/reaction_station.py index 2e2255d8..d35427d2 100644 --- a/unilabos/devices/workstation/bioyond_studio/reaction_station.py +++ b/unilabos/devices/workstation/bioyond_studio/reaction_station.py @@ -58,8 +58,8 @@ class BioyondReactionStation(BioyondWorkstation): Args: assign_material_name: 物料名称(不能为空) - cutoff: 截止值/通量配置(需为有效数字字符串,默认 "900000") - temperature: 温度上限(°C,范围:-50.00 至 100.00) + cutoff: 粘度上限(需为有效数字字符串,默认 "900000") + temperature: 温度设定(°C,范围:-50.00 至 100.00) Returns: str: JSON 字符串,格式为 {"suc": True} @@ -113,11 +113,11 @@ class BioyondReactionStation(BioyondWorkstation): """固体进料小瓶 Args: - material_id: 粉末类型ID + material_id: 粉末类型ID,1=盐(21分钟),2=面粉(27分钟),3=BTDA(38分钟) time: 观察时间(分钟) - torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是) + torque_variation: 是否观察(int类型, 1=否, 2=是) assign_material_name: 物料名称(用于获取试剂瓶位ID) - temperature: 温度上限(°C) + temperature: 温度设定(°C) """ self.append_to_workflow_sequence('{"web_workflow_name": "Solid_feeding_vials"}') material_id_m = self.hardware_interface._get_material_id_by_name(assign_material_name) if assign_material_name else None @@ -165,9 +165,9 @@ class BioyondReactionStation(BioyondWorkstation): Args: volume_formula: 分液公式(μL) assign_material_name: 物料名称 - titration_type: 是否滴定(1=滴定, 其他=非滴定) + titration_type: 是否滴定(1=否, 2=是) time: 观察时间(分钟) - torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是) + torque_variation: 是否观察(int类型, 1=否, 2=是) temperature: 温度(°C) """ self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_vials(non-titration)"}') @@ -208,7 +208,8 @@ class BioyondReactionStation(BioyondWorkstation): def liquid_feeding_solvents( self, assign_material_name: str, - volume: str, + volume: str = None, + solvents = None, titration_type: str = "1", time: str = "360", torque_variation: int = 2, @@ -218,12 +219,41 @@ class BioyondReactionStation(BioyondWorkstation): Args: assign_material_name: 物料名称 - volume: 分液量(μL) - titration_type: 是否滴定 + volume: 分液量(μL),直接指定体积(可选,如果提供solvents则自动计算) + solvents: 溶剂信息的字典或JSON字符串(可选),格式如下: + { + "additional_solvent": 33.55092503597727, # 溶剂体积(mL) + "total_liquid_volume": 48.00916988195499 + } + 如果提供solvents,则从中提取additional_solvent并转换为μL + titration_type: 是否滴定(1=否, 2=是) time: 观察时间(分钟) - torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是) - temperature: 温度上限(°C) + torque_variation: 是否观察(int类型, 1=否, 2=是) + temperature: 温度设定(°C) """ + # 处理 volume 参数:优先使用直接传入的 volume,否则从 solvents 中提取 + if volume is None and solvents is not None: + # 参数类型转换:如果是字符串则解析为字典 + if isinstance(solvents, str): + try: + solvents = json.loads(solvents) + except json.JSONDecodeError as e: + raise ValueError(f"solvents参数JSON解析失败: {str(e)}") + + # 参数验证 + if not isinstance(solvents, dict): + raise ValueError("solvents 必须是字典类型或有效的JSON字符串") + + # 提取 additional_solvent 值 + additional_solvent = solvents.get("additional_solvent") + if additional_solvent is None: + raise ValueError("solvents 中没有找到 additional_solvent 字段") + + # 转换为微升(μL) - 从毫升(mL)转换 + volume = str(float(additional_solvent) * 1000) + elif volume is None: + raise ValueError("必须提供 volume 或 solvents 参数之一") + self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding_solvents"}') material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) if material_id is None: @@ -273,9 +303,9 @@ class BioyondReactionStation(BioyondWorkstation): Args: volume_formula: 分液公式(μL) assign_material_name: 物料名称 - titration_type: 是否滴定 + titration_type: 是否滴定(1=否, 2=是) time: 观察时间(分钟) - torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是) + torque_variation: 是否观察(int类型, 1=否, 2=是) temperature: 温度(°C) """ self.append_to_workflow_sequence('{"web_workflow_name": "Liquid_feeding(titration)"}') @@ -328,9 +358,9 @@ class BioyondReactionStation(BioyondWorkstation): volume: 分液量(μL) assign_material_name: 物料名称(试剂瓶位) time: 观察时间(分钟) - torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是) - titration_type: 是否滴定 - temperature: 温度上限(°C) + torque_variation: 是否观察(int类型, 1=否, 2=是) + titration_type: 是否滴定(1=否, 2=是) + temperature: 温度设定(°C) """ self.append_to_workflow_sequence('{"web_workflow_name": "liquid_feeding_beaker"}') material_id = self.hardware_interface._get_material_id_by_name(assign_material_name) @@ -381,9 +411,9 @@ class BioyondReactionStation(BioyondWorkstation): Args: assign_material_name: 物料名称(液体种类) volume: 分液量(μL) - titration_type: 是否滴定 + titration_type: 是否滴定(1=否, 2=是) time: 观察时间(分钟) - torque_variation: 是否观察扭矩变化(int类型, 1=否, 2=是) + torque_variation: 是否观察(int类型, 1=否, 2=是) temperature: 温度(°C) """ self.append_to_workflow_sequence('{"web_workflow_name": "drip_back"}') @@ -605,7 +635,8 @@ class BioyondReactionStation(BioyondWorkstation): total_params += 1 step_parameters[step_id][action_name].append({ "Key": param_key, - "DisplayValue": param_value + "DisplayValue": param_value, + "Value": param_value }) successful_params += 1 # print(f" ✓ {param_key} = {param_value}") diff --git a/unilabos/devices/workstation/bioyond_studio/station.py b/unilabos/devices/workstation/bioyond_studio/station.py index 3e3e0b3b..e2f4da88 100644 --- a/unilabos/devices/workstation/bioyond_studio/station.py +++ b/unilabos/devices/workstation/bioyond_studio/station.py @@ -4,6 +4,7 @@ Bioyond Workstation Implementation 集成Bioyond物料管理的工作站示例 """ +import time import traceback from datetime import datetime from typing import Dict, Any, List, Optional, Union 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 6ea89398..6232fb8c 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py +++ b/unilabos/devices/workstation/coin_cell_assembly/YB_YH_materials.py @@ -973,4 +973,4 @@ def create_coin_cell_deck(name: str = "coin_cell_deck", size_x: float = 1000.0, """ deck = CoincellDeck(name=name, size_x=size_x, size_y=size_y, size_z=size_z) deck.setup() - return deck \ No newline at end of file + return deck diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/__init__.py b/unilabos/devices/workstation/coin_cell_assembly/__init__.py similarity index 100% rename from unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/__init__.py rename to unilabos/devices/workstation/coin_cell_assembly/__init__.py diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/button_battery_station.py b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/button_battery_station.py deleted file mode 100644 index eae09b84..00000000 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/button_battery_station.py +++ /dev/null @@ -1,1006 +0,0 @@ -""" -纽扣电池组装工作站物料类定义 -Button Battery Assembly Station Resource Classes -""" - -from __future__ import annotations - -from collections import OrderedDict -from typing import Any, Dict, List, Optional, TypedDict, Union, cast - -from pylabrobot.resources.coordinate import Coordinate -from pylabrobot.resources.container import Container -from pylabrobot.resources.deck import Deck -from pylabrobot.resources.itemized_resource import ItemizedResource -from pylabrobot.resources.resource import Resource -from pylabrobot.resources.resource_stack import ResourceStack -from pylabrobot.resources.tip_rack import TipRack, TipSpot -from pylabrobot.resources.trash import Trash -from pylabrobot.resources.utils import create_ordered_items_2d - - -class ElectrodeSheetState(TypedDict): - diameter: float # 直径 (mm) - thickness: float # 厚度 (mm) - mass: float # 质量 (g) - material_type: str # 材料类型(正极、负极、隔膜、弹片、垫片、铝箔等) - height: float - electrolyte_name: str - data_electrolyte_code: str - open_circuit_voltage: float - assembly_pressure: float - electrolyte_volume: float - - info: Optional[str] # 附加信息 - -class ElectrodeSheet(Resource): - """极片类 - 包含正负极片、隔膜、弹片、垫片、铝箔等所有片状材料""" - - def __init__( - self, - name: str = "极片", - size_x=10, - size_y=10, - size_z=10, - category: str = "electrode_sheet", - model: Optional[str] = None, - ): - """初始化极片 - - Args: - name: 极片名称 - category: 类别 - model: 型号 - """ - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - category=category, - model=model, - ) - self._unilabos_state: ElectrodeSheetState = ElectrodeSheetState( - diameter=14, - thickness=0.1, - mass=0.5, - material_type="copper", - info=None - ) - - # TODO: 这个还要不要?给self._unilabos_state赋值的? - def load_state(self, state: Dict[str, Any]) -> None: - """格式不变""" - super().load_state(state) - self._unilabos_state = state - #序列化 - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - """格式不变""" - data = super().serialize_state() - data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - -# TODO: 这个应该只能放一个极片 -class MaterialHoleState(TypedDict): - diameter: int - depth: int - max_sheets: int - info: Optional[str] # 附加信息 - -class MaterialHole(Resource): - """料板洞位类""" - children: List[ElectrodeSheet] = [] - - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - category: str = "material_hole", - **kwargs - ): - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - category=category, - ) - self._unilabos_state: MaterialHoleState = MaterialHoleState( - diameter=20, - depth=10, - max_sheets=1, - info=None - ) - - def get_all_sheet_info(self): - info_list = [] - for sheet in self.children: - info_list.append(sheet._unilabos_state["info"]) - return info_list - - #这个函数函数好像没用,一般不会集中赋值质量 - def set_all_sheet_mass(self): - for sheet in self.children: - sheet._unilabos_state["mass"] = 0.5 # 示例:设置质量为0.5g - - def load_state(self, state: Dict[str, Any]) -> None: - """格式不变""" - super().load_state(state) - self._unilabos_state = state - - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - """格式不变""" - data = super().serialize_state() - data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - #移动极片前先取出对象 - def get_sheet_with_name(self, name: str) -> Optional[ElectrodeSheet]: - for sheet in self.children: - if sheet.name == name: - return sheet - return None - - def has_electrode_sheet(self) -> bool: - """检查洞位是否有极片""" - return len(self.children) > 0 - - def assign_child_resource( - self, - resource: ElectrodeSheet, - location: Optional[Coordinate], - reassign: bool = True, - ): - """放置极片""" - # TODO: 这里要改,diameter找不到,加入._unilabos_state后应该没问题 - #if resource._unilabos_state["diameter"] > self._unilabos_state["diameter"]: - # raise ValueError(f"极片直径 {resource._unilabos_state['diameter']} 超过洞位直径 {self._unilabos_state['diameter']}") - #if len(self.children) >= self._unilabos_state["max_sheets"]: - # raise ValueError(f"洞位已满,无法放置更多极片") - super().assign_child_resource(resource, location, reassign) - - # 根据children的编号取物料对象。 - def get_electrode_sheet_info(self, index: int) -> ElectrodeSheet: - return self.children[index] - - - -class MaterialPlateState(TypedDict): - hole_spacing_x: float - hole_spacing_y: float - hole_diameter: float - info: Optional[str] # 附加信息 - -class MaterialPlate(ItemizedResource[MaterialHole]): - """料板类 - 4x4个洞位,每个洞位放1个极片""" - - children: List[MaterialHole] - - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - ordered_items: Optional[Dict[str, MaterialHole]] = None, - ordering: Optional[OrderedDict[str, str]] = None, - category: str = "material_plate", - model: Optional[str] = None, - fill: bool = False - ): - """初始化料板 - - Args: - name: 料板名称 - size_x: 长度 (mm) - size_y: 宽度 (mm) - size_z: 高度 (mm) - hole_diameter: 洞直径 (mm) - hole_depth: 洞深度 (mm) - hole_spacing_x: X方向洞位间距 (mm) - hole_spacing_y: Y方向洞位间距 (mm) - number: 编号 - category: 类别 - model: 型号 - """ - self._unilabos_state: MaterialPlateState = MaterialPlateState( - hole_spacing_x=24.0, - hole_spacing_y=24.0, - hole_diameter=20.0, - info="", - ) - # 创建4x4的洞位 - # TODO: 这里要改,对应不同形状 - holes = create_ordered_items_2d( - klass=MaterialHole, - num_items_x=4, - num_items_y=4, - dx=(size_x - 4 * self._unilabos_state["hole_spacing_x"]) / 2, # 居中 - dy=(size_y - 4 * self._unilabos_state["hole_spacing_y"]) / 2, # 居中 - dz=size_z, - item_dx=self._unilabos_state["hole_spacing_x"], - item_dy=self._unilabos_state["hole_spacing_y"], - size_x = 16, - size_y = 16, - size_z = 16, - ) - if fill: - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - ordered_items=holes, - category=category, - model=model, - ) - else: - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - ordered_items=ordered_items, - ordering=ordering, - category=category, - model=model, - ) - - def update_locations(self): - # TODO:调多次相加 - holes = create_ordered_items_2d( - klass=MaterialHole, - num_items_x=4, - num_items_y=4, - dx=(self._size_x - 3 * self._unilabos_state["hole_spacing_x"]) / 2, # 居中 - dy=(self._size_y - 3 * self._unilabos_state["hole_spacing_y"]) / 2, # 居中 - dz=self._size_z, - item_dx=self._unilabos_state["hole_spacing_x"], - item_dy=self._unilabos_state["hole_spacing_y"], - size_x = 1, - size_y = 1, - size_z = 1, - ) - for item, original_item in zip(holes.items(), self.children): - original_item.location = item[1].location - - -class PlateSlot(ResourceStack): - """板槽位类 - 1个槽上能堆放8个板,移板只能操作最上方的板""" - - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - max_plates: int = 8, - category: str = "plate_slot", - model: Optional[str] = None - ): - """初始化板槽位 - - Args: - name: 槽位名称 - max_plates: 最大板数量 - category: 类别 - """ - super().__init__( - name=name, - direction="z", # Z方向堆叠 - resources=[], - ) - self.max_plates = max_plates - self.category = category - - def can_add_plate(self) -> bool: - """检查是否可以添加板""" - return len(self.children) < self.max_plates - - def add_plate(self, plate: MaterialPlate) -> None: - """添加料板""" - if not self.can_add_plate(): - raise ValueError(f"槽位 {self.name} 已满,无法添加更多板") - self.assign_child_resource(plate) - - def get_top_plate(self) -> MaterialPlate: - """获取最上方的板""" - if len(self.children) == 0: - raise ValueError(f"槽位 {self.name} 为空") - return cast(MaterialPlate, self.get_top_item()) - - def take_top_plate(self) -> MaterialPlate: - """取出最上方的板""" - top_plate = self.get_top_plate() - self.unassign_child_resource(top_plate) - return top_plate - - def can_access_for_picking(self) -> bool: - """检查是否可以进行取料操作(只有最上方的板能进行取料操作)""" - return len(self.children) > 0 - - def serialize(self) -> dict: - return { - **super().serialize(), - "max_plates": self.max_plates, - } - - -class ClipMagazineHole(Container): - """子弹夹洞位类""" - - def __init__( - self, - name: str, - diameter: float, - depth: float, - max_sheets: int = 100, - category: str = "clip_magazine_hole", - ): - """初始化子弹夹洞位 - - Args: - name: 洞位名称 - diameter: 洞直径 (mm) - depth: 洞深度 (mm) - max_sheets: 最大极片数量 - category: 类别 - """ - super().__init__( - name=name, - size_x=diameter, - size_y=diameter, - size_z=depth, - category=category, - ) - self.diameter = diameter - self.depth = depth - self.max_sheets = max_sheets - self._sheets: List[ElectrodeSheet] = [] - - def can_add_sheet(self, sheet: ElectrodeSheet) -> bool: - """检查是否可以添加极片""" - return (len(self._sheets) < self.max_sheets and - sheet.diameter <= self.diameter) - - def add_sheet(self, sheet: ElectrodeSheet) -> None: - """添加极片""" - if not self.can_add_sheet(sheet): - raise ValueError(f"无法向洞位 {self.name} 添加极片") - self._sheets.append(sheet) - - def take_sheet(self) -> ElectrodeSheet: - """取出极片""" - if len(self._sheets) == 0: - raise ValueError(f"洞位 {self.name} 没有极片") - return self._sheets.pop() - - def get_sheet_count(self) -> int: - """获取极片数量""" - return len(self._sheets) - - def serialize_state(self) -> Dict[str, Any]: - return { - "sheet_count": len(self._sheets), - "sheets": [sheet.serialize() for sheet in self._sheets], - } - -# TODO: 这个要改 -class ClipMagazine(Resource): - """子弹夹类 - 有6个洞位,每个洞位放多个极片""" - - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - hole_spacing: float = 25.0, - max_sheets_per_hole: int = 100, - category: str = "clip_magazine", - model: Optional[str] = None, - ): - """初始化子弹夹 - - Args: - name: 子弹夹名称 - size_x: 长度 (mm) - size_y: 宽度 (mm) - size_z: 高度 (mm) - hole_diameter: 洞直径 (mm) - hole_depth: 洞深度 (mm) - hole_spacing: 洞位间距 (mm) - max_sheets_per_hole: 每个洞位最大极片数量 - category: 类别 - model: 型号 - """ - # 创建6个洞位,排成2x3布局 - holes = create_ordered_items_2d( - klass=ClipMagazineHole, - num_items_x=3, - num_items_y=2, - dx=(size_x - 2 * hole_spacing) / 2, # 居中 - dy=(size_y - hole_spacing) / 2, # 居中 - dz=size_z - 0, - item_dx=hole_spacing, - item_dy=hole_spacing, - diameter=0, - depth=0, - ) - - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - ordered_items=holes, - category=category, - model=model, - ) - - self.hole_diameter = hole_diameter - self.hole_depth = hole_depth - self.max_sheets_per_hole = max_sheets_per_hole - - def serialize(self) -> dict: - return { - **super().serialize(), - "hole_diameter": self.hole_diameter, - "hole_depth": self.hole_depth, - "max_sheets_per_hole": self.max_sheets_per_hole, - } -#是一种类型注解,不用self -class BatteryState(TypedDict): - """电池状态字典""" - diameter: float - height: float - assembly_pressure: float - electrolyte_volume: float - electrolyte_name: str - -class Battery(Resource): - """电池类 - 可容纳极片""" - children: List[ElectrodeSheet] = [] - - def __init__( - self, - name: str, - size_x=1, - size_y=1, - size_z=1, - category: str = "battery", - ): - """初始化电池 - - Args: - name: 电池名称 - diameter: 直径 (mm) - height: 高度 (mm) - max_volume: 最大容量 (μL) - barcode: 二维码编号 - category: 类别 - model: 型号 - """ - super().__init__( - name=name, - size_x=1, - size_y=1, - size_z=1, - category=category, - ) - self._unilabos_state: BatteryState = BatteryState( - diameter = 1.0, - height = 1.0, - assembly_pressure = 1.0, - electrolyte_volume = 1.0, - electrolyte_name = "DP001" - ) - - def add_electrolyte_with_bottle(self, bottle: Bottle) -> bool: - to_add_name = bottle._unilabos_state["electrolyte_name"] - if bottle.aspirate_electrolyte(10): - if self.add_electrolyte(to_add_name, 10): - pass - else: - bottle._unilabos_state["electrolyte_volume"] += 10 - - def set_electrolyte(self, name: str, volume: float) -> None: - """设置电解液信息""" - self._unilabos_state["electrolyte_name"] = name - self._unilabos_state["electrolyte_volume"] = volume - #这个应该没用,不会有加了后再加的事情 - def add_electrolyte(self, name: str, volume: float) -> bool: - """添加电解液信息""" - if name != self._unilabos_state["electrolyte_name"]: - return False - self._unilabos_state["electrolyte_volume"] += volume - - def load_state(self, state: Dict[str, Any]) -> None: - """格式不变""" - super().load_state(state) - self._unilabos_state = state - - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - """格式不变""" - data = super().serialize_state() - data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - -# 电解液作为属性放进去 - -class BatteryPressSlotState(TypedDict): - """电池状态字典""" - diameter: float =20.0 - depth: float = 4.0 - -class BatteryPressSlot(Resource): - """电池压制槽类 - 设备,可容纳一个电池""" - children: List[Battery] = [] - - def __init__( - self, - name: str = "BatteryPressSlot", - category: str = "battery_press_slot", - ): - """初始化电池压制槽 - - Args: - name: 压制槽名称 - diameter: 直径 (mm) - depth: 深度 (mm) - category: 类别 - model: 型号 - """ - super().__init__( - name=name, - size_x=10, - size_y=12, - size_z=13, - category=category, - ) - self._unilabos_state: BatteryPressSlotState = BatteryPressSlotState() - - def has_battery(self) -> bool: - """检查是否有电池""" - return len(self.children) > 0 - - def load_state(self, state: Dict[str, Any]) -> None: - """格式不变""" - super().load_state(state) - self._unilabos_state = state - - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - """格式不变""" - data = super().serialize_state() - data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - - def assign_child_resource( - self, - resource: Battery, - location: Optional[Coordinate], - reassign: bool = True, - ): - """放置极片""" - # TODO: 让高京看下槽位只有一个电池时是否这么写。 - if self.has_battery(): - raise ValueError(f"槽位已含有一个电池,无法再放置其他电池") - super().assign_child_resource(resource, location, reassign) - - # 根据children的编号取物料对象。 - def get_battery_info(self, index: int) -> Battery: - return self.children[0] - -# TODO:这个移液枪架子看一下从哪继承 -class TipBox64State(TypedDict): - """电池状态字典""" - tip_diameter: float = 5.0 - tip_length: float = 50.0 - with_tips: bool = True - -class TipBox64(TipRack): - """64孔枪头盒类""" - - children: List[TipSpot] = [] - def __init__( - self, - name: str, - size_x: float = 127.8, - size_y: float = 85.5, - size_z: float = 60.0, - category: str = "tip_box_64", - model: Optional[str] = None, - ): - """初始化64孔枪头盒 - - Args: - name: 枪头盒名称 - size_x: 长度 (mm) - size_y: 宽度 (mm) - size_z: 高度 (mm) - tip_diameter: 枪头直径 (mm) - tip_length: 枪头长度 (mm) - category: 类别 - model: 型号 - with_tips: 是否带枪头 - """ - from pylabrobot.resources.tip import Tip - - # 创建8x8=64个枪头位 - def make_tip(): - return Tip( - has_filter=False, - total_tip_length=20.0, - maximal_volume=1000, # 1mL - fitting_depth=8.0, - ) - - tip_spots = create_ordered_items_2d( - klass=TipSpot, - num_items_x=8, - num_items_y=8, - dx=8.0, - dy=8.0, - dz=0.0, - item_dx=9.0, - item_dy=9.0, - size_x=10, - size_y=10, - size_z=0.0, - make_tip=make_tip, - ) - self._unilabos_state: WasteTipBoxstate = WasteTipBoxstate() - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - ordered_items=tip_spots, - category=category, - model=model, - with_tips=True, - ) - - - -class WasteTipBoxstate(TypedDict): - """"废枪头盒状态字典""" - max_tips: int = 100 - tip_count: int = 0 - -#枪头不是一次性的(同一溶液则反复使用),根据寄存器判断 -class WasteTipBox(Trash): - """废枪头盒类 - 100个枪头容量""" - - def __init__( - self, - name: str, - size_x: float = 127.8, - size_y: float = 85.5, - size_z: float = 60.0, - category: str = "waste_tip_box", - model: Optional[str] = None, - ): - """初始化废枪头盒 - - Args: - name: 废枪头盒名称 - size_x: 长度 (mm) - size_y: 宽度 (mm) - size_z: 高度 (mm) - max_tips: 最大枪头容量 - category: 类别 - model: 型号 - """ - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - category=category, - model=model, - ) - self._unilabos_state: WasteTipBoxstate = WasteTipBoxstate() - - def add_tip(self) -> None: - """添加废枪头""" - if self._unilabos_state["tip_count"] >= self._unilabos_state["max_tips"]: - raise ValueError(f"废枪头盒 {self.name} 已满") - self._unilabos_state["tip_count"] += 1 - - def get_tip_count(self) -> int: - """获取枪头数量""" - return self._unilabos_state["tip_count"] - - def empty(self) -> None: - """清空废枪头盒""" - self._unilabos_state["tip_count"] = 0 - - - def load_state(self, state: Dict[str, Any]) -> None: - """格式不变""" - super().load_state(state) - self._unilabos_state = state - - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - """格式不变""" - data = super().serialize_state() - data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - - -class BottleRackState(TypedDict): - """ bottle_diameter: 瓶子直径 (mm) - bottle_height: 瓶子高度 (mm) - position_spacing: 位置间距 (mm)""" - bottle_diameter: float - bottle_height: float - name_to_index: dict - - - -class BottleRack(Resource): - """瓶架类 - 12个待配位置+12个已配位置""" - children: List[Bottle] = [] - - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - category: str = "bottle_rack", - model: Optional[str] = None, - ): - """初始化瓶架 - - Args: - name: 瓶架名称 - size_x: 长度 (mm) - size_y: 宽度 (mm) - size_z: 高度 (mm) - category: 类别 - model: 型号 - """ - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - category=category, - model=model, - ) - # TODO: 添加瓶位坐标映射 - self.index_to_pos = { - 0: Coordinate.zero(), - 1: Coordinate(x=1, y=2, z=3) # 添加 - } - self.name_to_index = {} - self.name_to_pos = {} - - def load_state(self, state: Dict[str, Any]) -> None: - """格式不变""" - super().load_state(state) - self._unilabos_state = state - - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - """格式不变""" - data = super().serialize_state() - data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - - # TODO: 这里有些问题要重新写一下 - def assign_child_resource(self, resource: Bottle, location=Coordinate.zero(), reassign = True): - assert len(self.children) <= 12, "瓶架已满,无法添加更多瓶子" - index = len(self.children) - location = Coordinate(x=20 + (index % 4) * 15, y=20 + (index // 4) * 15, z=0) - self.name_to_pos[resource.name] = location - self.name_to_index[resource.name] = index - return super().assign_child_resource(resource, location, reassign) - - def assign_child_resource_by_index(self, resource: Bottle, index: int): - assert 0 <= index < 12, "无效的瓶子索引" - self.name_to_index[resource.name] = index - location = self.index_to_pos[index] - return super().assign_child_resource(resource, location) - - def unassign_child_resource(self, resource: Bottle): - super().unassign_child_resource(resource) - self.index_to_pos.pop(self.name_to_index.pop(resource.name, None), None) - - # def serialize(self): - # self.children.sort(key=lambda x: self.name_to_index.get(x.name, 0)) - # return super().serialize() - - -class BottleState(TypedDict): - diameter: float - height: float - electrolyte_name: str - electrolyte_volume: float - max_volume: float - -class Bottle(Resource): - """瓶子类 - 容纳电解液""" - - def __init__( - self, - name: str, - category: str = "bottle", - ): - """初始化瓶子 - - Args: - name: 瓶子名称 - diameter: 直径 (mm) - height: 高度 (mm) - max_volume: 最大体积 (μL) - barcode: 二维码 - category: 类别 - model: 型号 - """ - super().__init__( - name=name, - size_x=1, - size_y=1, - size_z=1, - category=category, - ) - self._unilabos_state: BottleState = BottleState() - - def aspirate_electrolyte(self, volume: float) -> bool: - current_volume = self._unilabos_state["electrolyte_volume"] - assert current_volume > volume, f"Cannot aspirate {volume}μL, only {current_volume}μL available." - self._unilabos_state["electrolyte_volume"] -= volume - return True - - def load_state(self, state: Dict[str, Any]) -> None: - """格式不变""" - super().load_state(state) - self._unilabos_state = state - - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - """格式不变""" - data = super().serialize_state() - data.update(self._unilabos_state) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - -class CoincellDeck(Deck): - """纽扣电池组装工作站台面类""" - - def __init__( - self, - name: str = "coin_cell_deck", - size_x: float = 1620.0, # 3.66m - size_y: float = 1270.0, # 1.23m - size_z: float = 500.0, - origin: Coordinate = Coordinate(0, 0, 0), - category: str = "coin_cell_deck", - ): - """初始化纽扣电池组装工作站台面 - - Args: - name: 台面名称 - size_x: 长度 (mm) - 3.66m - size_y: 宽度 (mm) - 1.23m - size_z: 高度 (mm) - origin: 原点坐标 - category: 类别 - """ - super().__init__( - name=name, - size_x=size_x, - size_y=size_y, - size_z=size_z, - origin=origin, - category=category, - ) - -#if __name__ == "__main__": -# # 转移极片的测试代码 -# deck = CoincellDeck("coin_cell_deck") -# ban_cao_wei = PlateSlot("ban_cao_wei", max_plates=8) -# deck.assign_child_resource(ban_cao_wei, Coordinate(x=0, y=0, z=0)) -# -# plate_1 = MaterialPlate("plate_1", 1,1,1, fill=True) -# for i, hole in enumerate(plate_1.children): -# sheet = ElectrodeSheet(f"hole_{i}_sheet_1") -# sheet._unilabos_state = { -# "diameter": 14, -# "info": "NMC", -# "mass": 5.0, -# "material_type": "positive_electrode", -# "thickness": 0.1 -# } -# hole._unilabos_state = { -# "depth": 1.0, -# "diameter": 14, -# "info": "", -# "max_sheets": 1 -# } -# hole.assign_child_resource(sheet, Coordinate.zero()) -# plate_1._unilabos_state = { -# "hole_spacing_x": 20.0, -# "hole_spacing_y": 20.0, -# "hole_diameter": 5, -# "info": "这是第一块料板" -# } -# plate_1.update_locations() -# ban_cao_wei.assign_child_resource(plate_1, Coordinate.zero()) -# # zi_dan_jia = ClipMagazine("zi_dan_jia", 1, 1, 1) -# # deck.assign_child_resource(ban_cao_wei, Coordinate(x=200, y=200, z=0)) -# -# from unilabos.resources.graphio import * -# A = tree_to_list([resource_plr_to_ulab(deck)]) -# with open("test.json", "w") as f: -# json.dump(A, f) -# -# -#def get_plate_with_14mm_hole(name=""): -# plate = MaterialPlate(name=name) -# for i in range(4): -# for j in range(4): -# hole = MaterialHole(f"{i+1}x{j+1}") -# hole._unilabos_state["diameter"] = 14 -# hole._unilabos_state["max_sheets"] = 1 -# plate.assign_child_resource(hole) -# return plate - -import json - -if __name__ == "__main__": - #electrode1 = BatteryPressSlot() - #print(electrode1.get_size_x()) - #print(electrode1.get_size_y()) - #print(electrode1.get_size_z()) - #jipian = ElectrodeSheet() - #jipian._unilabos_state["diameter"] = 18 - #print(jipian.serialize()) - #print(jipian.serialize_state()) - - deck = CoincellDeck(size_x=1000, - size_y=1000, - size_z=900) - - #liaopan = TipBox64(name="liaopan") - - #创建一个4*4的物料板 - liaopan1 = MaterialPlate(name="liaopan1", size_x=120.8, size_y=120.5, size_z=10.0, fill=True) - #把物料板放到桌子上 - deck.assign_child_resource(liaopan1, Coordinate(x=0, y=0, z=0)) - #创建一个极片 - for i in range(16): - jipian = ElectrodeSheet(name=f"jipian1_{i}", size_x= 12, size_y=12, size_z=0.1) - liaopan1.children[i].assign_child_resource(jipian, location=None) -# - #创建一个4*4的物料板 - liaopan2 = MaterialPlate(name="liaopan2", size_x=120.8, size_y=120.5, size_z=10.0, fill=True) - #把物料板放到桌子上 - deck.assign_child_resource(liaopan2, Coordinate(x=500, y=0, z=0)) - - #创建一个4*4的物料板 - liaopan3 = MaterialPlate(name="电池料盘", size_x=120.8, size_y=160.5, size_z=10.0, fill=True) - #把物料板放到桌子上 - deck.assign_child_resource(liaopan3, Coordinate(x=100, y=100, z=0)) - - - - #liaopan.children[3].assign_child_resource(jipian, location=None) - print(deck) - - - from unilabos.resources.graphio import convert_resources_from_type - from unilabos.config.config import BasicConfig - BasicConfig.ak = "4d5ce6ae-7234-4639-834e-93899b9caf94" - BasicConfig.sk = "505d3b0a-620e-459a-9905-1efcffce382a" - from unilabos.app.web.client import http_client - - resources = convert_resources_from_type([deck], [Resource]) - json.dump({"nodes": resources, "links": []}, open("button_battery_station_resources_unilab.json", "w"), indent=2) - - - #print(resources) - http_client.remote_addr = "https://uni-lab.test.bohrium.com/api/v1" - - http_client.resource_add(resources) \ No newline at end of file diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/coin_cell_assembly.py b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/coin_cell_assembly.py deleted file mode 100644 index 750a34fb..00000000 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/coin_cell_assembly.py +++ /dev/null @@ -1,1172 +0,0 @@ -import csv -import json -import os -import threading -import time -from datetime import datetime -from typing import Any, Dict, Optional -from pylabrobot.resources import Resource as PLRResource -from unilabos_msgs.msg import Resource -from unilabos.device_comms.modbus_plc.client import ModbusTcpClient -from unilabos.devices.workstation.coin_cell_assembly.button_battery_station import MaterialHole, MaterialPlate -from unilabos.devices.workstation.workstation_base import WorkstationBase -from unilabos.device_comms.modbus_plc.client import TCPClient, ModbusNode, PLCWorkflow, ModbusWorkflow, WorkflowAction, BaseClient -from unilabos.device_comms.modbus_plc.modbus import DeviceType, Base as ModbusNodeBase, DataType, WorderOrder -from unilabos.devices.workstation.coin_cell_assembly.button_battery_station import * -from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, BaseROS2DeviceNode -from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode - -#构建物料系统 - -class CoinCellAssemblyWorkstation(WorkstationBase): - def __init__( - self, - station_resource: CoincellDeck, - address: str = "192.168.1.20", - port: str = "502", - debug_mode: bool = True, - *args, - **kwargs, - ): - super().__init__( - #桌子 - station_resource=station_resource, - *args, - **kwargs, - ) - self.debug_mode = debug_mode - self.station_resource = station_resource - """ 连接初始化 """ - modbus_client = TCPClient(addr=address, port=port) - print("modbus_client", modbus_client) - if not debug_mode: - modbus_client.client.connect() - count = 100 - while count >0: - count -=1 - if modbus_client.client.is_socket_open(): - break - time.sleep(2) - if not modbus_client.client.is_socket_open(): - raise ValueError('modbus tcp connection failed') - else: - print("测试模式,跳过连接") - - """ 工站的配置 """ - self.nodes = BaseClient.load_csv(os.path.join(os.path.dirname(__file__), 'coin_cell_assembly_a.csv')) - self.client = modbus_client.register_node_list(self.nodes) - self.success = False - self.allow_data_read = False #允许读取函数运行标志位 - self.csv_export_thread = None - self.csv_export_running = False - self.csv_export_file = None - self.coin_num_N = 0 #已组装电池数量 - #创建一个物料台面,包含两个极片板 - #self.deck = create_a_coin_cell_deck() - - #self._ros_node.update_resource(self.deck) - - #ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ - # "resources": [self.deck] - #}) - - - def post_init(self, ros_node: ROS2WorkstationNode): - self._ros_node = ros_node - #self.deck = create_a_coin_cell_deck() - ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ - "resources": [self.station_resource] - }) - - # 批量操作在这里写 - async def change_hole_sheet_to_2(self, hole: MaterialHole): - hole._unilabos_state["max_sheets"] = 2 - return await self._ros_node.update_resource(hole) - - - async def fill_plate(self): - plate_1: MaterialPlate = self.station_resource.children[0].children[0] - #plate_1 - return await self._ros_node.update_resource(plate_1) - - #def run_assembly(self, wf_name: str, resource: PLRResource, params: str = "\{\}"): - # """启动工作流""" - # self.current_workflow_status = WorkflowStatus.RUNNING - # logger.info(f"工作站 {self.device_id} 启动工作流: {wf_name}") -# - # # TODO: 实现工作流逻辑 -# - # anode_sheet = self.deck.get_resource("anode_sheet") - - """ Action逻辑代码 """ - def _sys_start_cmd(self, cmd=None): - """设备启动命令 (可读写)""" - if cmd is not None: # 写入模式 - self.success = False - node = self.client.use_node('COIL_SYS_START_CMD') - ret = node.write(cmd) - print(ret) - self.success = True - return self.success - else: # 读取模式 - cmd_feedback, read_err = self.client.use_node('COIL_SYS_START_CMD').read(1) - return cmd_feedback[0] - - def _sys_stop_cmd(self, cmd=None): - """设备停止命令 (可读写)""" - if cmd is not None: # 写入模式 - self.success = False - node = self.client.use_node('COIL_SYS_STOP_CMD') - node.write(cmd) - self.success = True - return self.success - else: # 读取模式 - cmd_feedback, read_err = self.client.use_node('COIL_SYS_STOP_CMD').read(1) - return cmd_feedback[0] - - def _sys_reset_cmd(self, cmd=None): - """设备复位命令 (可读写)""" - if cmd is not None: - self.success = False - self.client.use_node('COIL_SYS_RESET_CMD').write(cmd) - self.success = True - return self.success - else: - cmd_feedback, read_err = self.client.use_node('COIL_SYS_RESET_CMD').read(1) - return cmd_feedback[0] - - def _sys_hand_cmd(self, cmd=None): - """手动模式命令 (可读写)""" - if cmd is not None: - self.success = False - self.client.use_node('COIL_SYS_HAND_CMD').write(cmd) - self.success = True - print("步骤0") - return self.success - else: - cmd_feedback, read_err = self.client.use_node('COIL_SYS_HAND_CMD').read(1) - return cmd_feedback[0] - - def _sys_auto_cmd(self, cmd=None): - """自动模式命令 (可读写)""" - if cmd is not None: - self.success = False - self.client.use_node('COIL_SYS_AUTO_CMD').write(cmd) - self.success = True - return self.success - else: - cmd_feedback, read_err = self.client.use_node('COIL_SYS_AUTO_CMD').read(1) - return cmd_feedback[0] - - def _sys_init_cmd(self, cmd=None): - """初始化命令 (可读写)""" - if cmd is not None: - self.success = False - self.client.use_node('COIL_SYS_INIT_CMD').write(cmd) - self.success = True - return self.success - else: - cmd_feedback, read_err = self.client.use_node('COIL_SYS_INIT_CMD').read(1) - return cmd_feedback[0] - - def _unilab_send_msg_succ_cmd(self, cmd=None): - """UNILAB发送配方完毕 (可读写)""" - if cmd is not None: - self.success = False - self.client.use_node('COIL_UNILAB_SEND_MSG_SUCC_CMD').write(cmd) - self.success = True - return self.success - else: - cmd_feedback, read_err = self.client.use_node('COIL_UNILAB_SEND_MSG_SUCC_CMD').read(1) - return cmd_feedback[0] - - def _unilab_rec_msg_succ_cmd(self, cmd=None): - """UNILAB接收测试电池数据完毕 (可读写)""" - if cmd is not None: - self.success = False - self.client.use_node('COIL_UNILAB_REC_MSG_SUCC_CMD').write(cmd) - self.success = True - return self.success - else: - cmd_feedback, read_err = self.client.use_node('COIL_UNILAB_REC_MSG_SUCC_CMD').read(1) - return cmd_feedback - - - # ====================== 命令类指令(REG_x_) ====================== - def _unilab_send_msg_electrolyte_num(self, num=None): - """UNILAB写电解液使用瓶数(可读写)""" - if num is not None: - self.success = False - ret = self.client.use_node('REG_MSG_ELECTROLYTE_NUM').write(num) - print(ret) - self.success = True - return self.success - else: - cmd_feedback, read_err = self.client.use_node('REG_MSG_ELECTROLYTE_NUM').read(1) - return cmd_feedback[0] - - def _unilab_send_msg_electrolyte_use_num(self, use_num=None): - """UNILAB写单次电解液使用瓶数(可读写)""" - if use_num is not None: - self.success = False - self.client.use_node('REG_MSG_ELECTROLYTE_USE_NUM').write(use_num) - self.success = True - return self.success - else: - return False - - def _unilab_send_msg_assembly_type(self, num=None): - """UNILAB写组装参数""" - if num is not None: - self.success = False - self.client.use_node('REG_MSG_ASSEMBLY_TYPE').write(num) - self.success = True - return self.success - else: - cmd_feedback, read_err = self.client.use_node('REG_MSG_ASSEMBLY_TYPE').read(1) - return cmd_feedback[0] - - def _unilab_send_msg_electrolyte_vol(self, vol=None): - """UNILAB写电解液吸取量参数""" - if vol is not None: - self.success = False - self.client.use_node('REG_MSG_ELECTROLYTE_VOLUME').write(vol, data_type=DataType.FLOAT32, word_order=WorderOrder.LITTLE) - self.success = True - return self.success - else: - cmd_feedback, read_err = self.client.use_node('REG_MSG_ELECTROLYTE_VOLUME').read(2, word_order=WorderOrder.LITTLE) - return cmd_feedback[0] - - def _unilab_send_msg_assembly_pressure(self, vol=None): - """UNILAB写电池压制力""" - if vol is not None: - self.success = False - self.client.use_node('REG_MSG_ASSEMBLY_PRESSURE').write(vol, data_type=DataType.FLOAT32, word_order=WorderOrder.LITTLE) - self.success = True - return self.success - else: - cmd_feedback, read_err = self.client.use_node('REG_MSG_ASSEMBLY_PRESSURE').read(2, word_order=WorderOrder.LITTLE) - return cmd_feedback[0] - - # ==================== 0905新增内容(COIL_x_STATUS) ==================== - def _unilab_send_electrolyte_bottle_num(self, num=None): - """UNILAB发送电解液瓶数完毕""" - if num is not None: - self.success = False - self.client.use_node('UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM').write(num) - self.success = True - return self.success - else: - cmd_feedback, read_err = self.client.use_node('UNILAB_SEND_ELECTROLYTE_BOTTLE_NUM').read(1) - return cmd_feedback[0] - - def _unilab_rece_electrolyte_bottle_num(self, num=None): - """设备请求接受电解液瓶数""" - if num is not None: - self.success = False - self.client.use_node('UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM').write(num) - self.success = True - return self.success - else: - cmd_feedback, read_err = self.client.use_node('UNILAB_RECE_ELECTROLYTE_BOTTLE_NUM').read(1) - return cmd_feedback[0] - - def _reg_msg_electrolyte_num(self, num=None): - """电解液已使用瓶数""" - if num is not None: - self.success = False - self.client.use_node('REG_MSG_ELECTROLYTE_NUM').write(num) - self.success = True - return self.success - else: - cmd_feedback, read_err = self.client.use_node('REG_MSG_ELECTROLYTE_NUM').read(1) - return cmd_feedback[0] - - def _reg_data_electrolyte_use_num(self, num=None): - """单瓶电解液完成组装数""" - if num is not None: - self.success = False - self.client.use_node('REG_DATA_ELECTROLYTE_USE_NUM').write(num) - self.success = True - return self.success - else: - cmd_feedback, read_err = self.client.use_node('REG_DATA_ELECTROLYTE_USE_NUM').read(1) - return cmd_feedback[0] - - def _unilab_send_finished_cmd(self, num=None): - """Unilab发送已知一组组装完成信号""" - if num is not None: - self.success = False - self.client.use_node('UNILAB_SEND_FINISHED_CMD').write(num) - self.success = True - return self.success - else: - cmd_feedback, read_err = self.client.use_node('UNILAB_SEND_FINISHED_CMD').read(1) - return cmd_feedback[0] - - def _unilab_rece_finished_cmd(self, num=None): - """Unilab接收已知一组组装完成信号""" - if num is not None: - self.success = False - self.client.use_node('UNILAB_RECE_FINISHED_CMD').write(num) - self.success = True - return self.success - else: - cmd_feedback, read_err = self.client.use_node('UNILAB_RECE_FINISHED_CMD').read(1) - return cmd_feedback[0] - - - - # ==================== 状态类属性(COIL_x_STATUS) ==================== - def _sys_start_status(self) -> bool: - """设备启动中( BOOL)""" - status, read_err = self.client.use_node('COIL_SYS_START_STATUS').read(1) - return status[0] - - def _sys_stop_status(self) -> bool: - """设备停止中( BOOL)""" - status, read_err = self.client.use_node('COIL_SYS_STOP_STATUS').read(1) - return status[0] - - def _sys_reset_status(self) -> bool: - """设备复位中( BOOL)""" - status, read_err = self.client.use_node('COIL_SYS_RESET_STATUS').read(1) - return status[0] - - def _sys_init_status(self) -> bool: - """设备初始化完成( BOOL)""" - status, read_err = self.client.use_node('COIL_SYS_INIT_STATUS').read(1) - return status[0] - - # 查找资源 - def modify_deck_name(self, resource_name: str): - # figure_res = self._ros_node.resource_tracker.figure_resource({"name": resource_name}) - # print(f"!!! figure_res: {type(figure_res)}") - self.station_resource.children[1] - return - - @property - def sys_status(self) -> str: - if self.debug_mode: - return "设备调试模式" - if self._sys_start_status(): - return "设备启动中" - elif self._sys_stop_status(): - return "设备停止中" - elif self._sys_reset_status(): - return "设备复位中" - elif self._sys_init_status(): - return "设备初始化中" - else: - return "未知状态" - - def _sys_hand_status(self) -> bool: - """设备手动模式( BOOL)""" - status, read_err = self.client.use_node('COIL_SYS_HAND_STATUS').read(1) - return status[0] - - def _sys_auto_status(self) -> bool: - """设备自动模式( BOOL)""" - status, read_err = self.client.use_node('COIL_SYS_AUTO_STATUS').read(1) - return status[0] - - @property - def sys_mode(self) -> str: - if self.debug_mode: - return "设备调试模式" - if self._sys_hand_status(): - return "设备手动模式" - elif self._sys_auto_status(): - return "设备自动模式" - else: - return "未知模式" - - @property - def request_rec_msg_status(self) -> bool: - """设备请求接受配方( BOOL)""" - if self.debug_mode: - return True - status, read_err = self.client.use_node('COIL_REQUEST_REC_MSG_STATUS').read(1) - return status[0] - - @property - def request_send_msg_status(self) -> bool: - """设备请求发送测试数据( BOOL)""" - if self.debug_mode: - return True - status, read_err = self.client.use_node('COIL_REQUEST_SEND_MSG_STATUS').read(1) - return status[0] - - # ======================= 其他属性(特殊功能) ======================== - ''' - @property - def warning_1(self) -> bool: - status, read_err = self.client.use_node('COIL_WARNING_1').read(1) - return status[0] - ''' - # ===================== 生产数据区 ====================== - - @property - def data_assembly_coin_cell_num(self) -> int: - """已完成电池数量 (INT16)""" - if self.debug_mode: - return 0 - num, read_err = self.client.use_node('REG_DATA_ASSEMBLY_COIN_CELL_NUM').read(1) - return num - - @property - def data_assembly_time(self) -> float: - """单颗电池组装时间 (秒, REAL/FLOAT32)""" - if self.debug_mode: - return 0 - time, read_err = self.client.use_node('REG_DATA_ASSEMBLY_PER_TIME').read(2, word_order=WorderOrder.LITTLE) - return time - - @property - def data_open_circuit_voltage(self) -> float: - """开路电压值 (FLOAT32)""" - if self.debug_mode: - return 0 - vol, read_err = self.client.use_node('REG_DATA_OPEN_CIRCUIT_VOLTAGE').read(2, word_order=WorderOrder.LITTLE) - return vol - - @property - def data_axis_x_pos(self) -> float: - """分液X轴当前位置 (FLOAT32)""" - if self.debug_mode: - return 0 - pos, read_err = self.client.use_node('REG_DATA_AXIS_X_POS').read(2, word_order=WorderOrder.LITTLE) - return pos - - @property - def data_axis_y_pos(self) -> float: - """分液Y轴当前位置 (FLOAT32)""" - if self.debug_mode: - return 0 - pos, read_err = self.client.use_node('REG_DATA_AXIS_Y_POS').read(2, word_order=WorderOrder.LITTLE) - return pos - - @property - def data_axis_z_pos(self) -> float: - """分液Z轴当前位置 (FLOAT32)""" - if self.debug_mode: - return 0 - pos, read_err = self.client.use_node('REG_DATA_AXIS_Z_POS').read(2, word_order=WorderOrder.LITTLE) - return pos - - @property - def data_pole_weight(self) -> float: - """当前电池正极片称重数据 (FLOAT32)""" - if self.debug_mode: - return 0 - weight, read_err = self.client.use_node('REG_DATA_POLE_WEIGHT').read(2, word_order=WorderOrder.LITTLE) - return weight - - @property - def data_assembly_pressure(self) -> int: - """当前电池压制力 (INT16)""" - if self.debug_mode: - return 0 - pressure, read_err = self.client.use_node('REG_DATA_ASSEMBLY_PRESSURE').read(1) - return pressure - - @property - def data_electrolyte_volume(self) -> int: - """当前电解液加注量 (INT16)""" - if self.debug_mode: - return 0 - vol, read_err = self.client.use_node('REG_DATA_ELECTROLYTE_VOLUME').read(1) - return vol - - @property - def data_coin_num(self) -> int: - """当前电池数量 (INT16)""" - if self.debug_mode: - return 0 - num, read_err = self.client.use_node('REG_DATA_COIN_NUM').read(1) - return num - - @property - def data_coin_cell_code(self) -> str: - """电池二维码序列号 (STRING)""" - try: - # 尝试不同的字节序读取 - code_little, read_err = self.client.use_node('REG_DATA_COIN_CELL_CODE').read(10, word_order=WorderOrder.LITTLE) - print(code_little) - clean_code = code_little[-8:][::-1] - return clean_code - except Exception as e: - print(f"读取电池二维码失败: {e}") - return "N/A" - - - @property - def data_electrolyte_code(self) -> str: - try: - # 尝试不同的字节序读取 - code_little, read_err = self.client.use_node('REG_DATA_ELECTROLYTE_CODE').read(10, word_order=WorderOrder.LITTLE) - print(code_little) - clean_code = code_little[-8:][::-1] - return clean_code - except Exception as e: - print(f"读取电解液二维码失败: {e}") - return "N/A" - - # ===================== 环境监控区 ====================== - @property - def data_glove_box_pressure(self) -> float: - """手套箱压力 (bar, FLOAT32)""" - if self.debug_mode: - return 0 - status, read_err = self.client.use_node('REG_DATA_GLOVE_BOX_PRESSURE').read(2, word_order=WorderOrder.LITTLE) - return status - - @property - def data_glove_box_o2_content(self) -> float: - """手套箱氧含量 (ppm, FLOAT32)""" - if self.debug_mode: - return 0 - value, read_err = self.client.use_node('REG_DATA_GLOVE_BOX_O2_CONTENT').read(2, word_order=WorderOrder.LITTLE) - return value - - @property - def data_glove_box_water_content(self) -> float: - """手套箱水含量 (ppm, FLOAT32)""" - if self.debug_mode: - return 0 - value, read_err = self.client.use_node('REG_DATA_GLOVE_BOX_WATER_CONTENT').read(2, word_order=WorderOrder.LITTLE) - return value - -# @property -# def data_stack_vision_code(self) -> int: -# """物料堆叠复检图片编码 (INT16)""" -# if self.debug_mode: -# return 0 -# code, read_err = self.client.use_node('REG_DATA_STACK_VISON_CODE').read(1) -# #code, _ = self.client.use_node('REG_DATA_STACK_VISON_CODE').read(1).type -# print(f"读取物料堆叠复检图片编码", {code}, "error", type(code)) -# #print(code.type) -# # print(read_err) -# return int(code) - - def func_pack_device_init(self): - #切换手动模式 - print("切换手动模式") - self._sys_hand_cmd(True) - time.sleep(1) - while (self._sys_hand_status()) == False: - print("waiting for hand_cmd") - time.sleep(1) - #设备初始化 - self._sys_init_cmd(True) - time.sleep(1) - #sys_init_status为bool值,不加括号 - while (self._sys_init_status())== False: - print("waiting for init_cmd") - time.sleep(1) - #手动按钮置回False - self._sys_hand_cmd(False) - time.sleep(1) - while (self._sys_hand_cmd()) == True: - print("waiting for hand_cmd to False") - time.sleep(1) - #初始化命令置回False - self._sys_init_cmd(False) - time.sleep(1) - while (self._sys_init_cmd()) == True: - print("waiting for init_cmd to False") - time.sleep(1) - - def func_pack_device_auto(self): - #切换自动 - print("切换自动模式") - self._sys_auto_cmd(True) - time.sleep(1) - while (self._sys_auto_status()) == False: - print("waiting for auto_status") - time.sleep(1) - #自动按钮置False - self._sys_auto_cmd(False) - time.sleep(1) - while (self._sys_auto_cmd()) == True: - print("waiting for auto_cmd") - time.sleep(1) - - def func_pack_device_start(self): - #切换自动 - print("启动") - self._sys_start_cmd(True) - time.sleep(1) - while (self._sys_start_status()) == False: - print("waiting for start_status") - time.sleep(1) - #自动按钮置False - self._sys_start_cmd(False) - time.sleep(1) - while (self._sys_start_cmd()) == True: - print("waiting for start_cmd") - time.sleep(1) - - def func_pack_send_bottle_num(self, bottle_num): - bottle_num = int(bottle_num) - #发送电解液平台数 - print("启动") - while (self._unilab_rece_electrolyte_bottle_num()) == False: - print("waiting for rece_electrolyte_bottle_num to True") - # self.client.use_node('8520').write(True) - time.sleep(1) - #发送电解液瓶数为2 - self._reg_msg_electrolyte_num(bottle_num) - time.sleep(1) - #完成信号置True - self._unilab_send_electrolyte_bottle_num(True) - time.sleep(1) - #检测到依华已接收 - while (self._unilab_rece_electrolyte_bottle_num()) == True: - print("waiting for rece_electrolyte_bottle_num to False") - time.sleep(1) - #完成信号置False - self._unilab_send_electrolyte_bottle_num(False) - time.sleep(1) - #自动按钮置False - - - # 下发参数 - #def func_pack_send_msg_cmd(self, elec_num: int, elec_use_num: int, elec_vol: float, assembly_type: int, assembly_pressure: int) -> bool: - # """UNILAB写参数""" - # while (self.request_rec_msg_status) == False: - # print("wait for res_msg") - # time.sleep(1) - # self.success = False - # self._unilab_send_msg_electrolyte_num(elec_num) - # time.sleep(1) - # self._unilab_send_msg_electrolyte_use_num(elec_use_num) - # time.sleep(1) - # self._unilab_send_msg_electrolyte_vol(elec_vol) - # time.sleep(1) - # self._unilab_send_msg_assembly_type(assembly_type) - # time.sleep(1) - # self._unilab_send_msg_assembly_pressure(assembly_pressure) - # time.sleep(1) - # self._unilab_send_msg_succ_cmd(True) - # time.sleep(1) - # self._unilab_send_msg_succ_cmd(False) - # #将允许读取标志位置True - # self.allow_data_read = True - # self.success = True - # return self.success - - def func_pack_send_msg_cmd(self, elec_use_num, elec_vol, assembly_type, assembly_pressure) -> bool: - """UNILAB写参数""" - while (self.request_rec_msg_status) == False: - print("wait for request_rec_msg_status to True") - time.sleep(1) - self.success = False - #self._unilab_send_msg_electrolyte_num(elec_num) - #设置平行样数目 - self._unilab_send_msg_electrolyte_use_num(elec_use_num) - time.sleep(1) - #发送电解液加注量 - self._unilab_send_msg_electrolyte_vol(elec_vol) - time.sleep(1) - #发送电解液组装类型 - self._unilab_send_msg_assembly_type(assembly_type) - time.sleep(1) - #发送电池压制力 - self._unilab_send_msg_assembly_pressure(assembly_pressure) - time.sleep(1) - self._unilab_send_msg_succ_cmd(True) - time.sleep(1) - while (self.request_rec_msg_status) == True: - print("wait for request_rec_msg_status to False") - time.sleep(1) - self._unilab_send_msg_succ_cmd(False) - #将允许读取标志位置True - self.allow_data_read = True - self.success = True - return self.success - - def func_pack_get_msg_cmd(self, file_path: str="D:\\coin_cell_data") -> bool: - """UNILAB读参数""" - while self.request_send_msg_status == False: - print("waiting for send_read_msg_status to True") - time.sleep(1) - data_open_circuit_voltage = self.data_open_circuit_voltage - data_pole_weight = self.data_pole_weight - data_assembly_time = self.data_assembly_time - data_assembly_pressure = self.data_assembly_pressure - data_electrolyte_volume = self.data_electrolyte_volume - data_coin_num = self.data_coin_num - data_electrolyte_code = self.data_electrolyte_code - data_coin_cell_code = self.data_coin_cell_code - print("data_open_circuit_voltage", data_open_circuit_voltage) - print("data_pole_weight", data_pole_weight) - print("data_assembly_time", data_assembly_time) - print("data_assembly_pressure", data_assembly_pressure) - print("data_electrolyte_volume", data_electrolyte_volume) - print("data_coin_num", data_coin_num) - print("data_electrolyte_code", data_electrolyte_code) - print("data_coin_cell_code", data_coin_cell_code) - #接收完信息后,读取完毕标志位置True - liaopan3 = self.station_resource.get_resource("\u7535\u6c60\u6599\u76d8") - #把物料解绑后放到另一盘上 - battery = ElectrodeSheet(name=f"battery_{self.coin_num_N}", size_x=14, size_y=14, size_z=2) - battery._unilabos_state = { - "electrolyte_name": data_coin_cell_code, - "data_electrolyte_code": data_electrolyte_code, - "open_circuit_voltage": data_open_circuit_voltage, - "assembly_pressure": data_assembly_pressure, - "electrolyte_volume": data_electrolyte_volume - } - liaopan3.children[self.coin_num_N].assign_child_resource(battery, location=None) - #print(jipian2.parent) - ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ - "resources": [self.station_resource] - }) - - - self._unilab_rec_msg_succ_cmd(True) - time.sleep(1) - #等待允许读取标志位置False - while self.request_send_msg_status == True: - print("waiting for send_msg_status to False") - time.sleep(1) - self._unilab_rec_msg_succ_cmd(False) - time.sleep(1) - #将允许读取标志位置True - time_date = datetime.now().strftime("%Y%m%d") - #秒级时间戳用于标记每一行电池数据 - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - #生成输出文件的变量 - self.csv_export_file = os.path.join(file_path, f"date_{time_date}.csv") - #将数据存入csv文件 - if not os.path.exists(self.csv_export_file): - #创建一个表头 - with open(self.csv_export_file, 'w', newline='', encoding='utf-8') as csvfile: - writer = csv.writer(csvfile) - writer.writerow([ - 'Time', 'open_circuit_voltage', 'pole_weight', - 'assembly_time', 'assembly_pressure', 'electrolyte_volume', - 'coin_num', 'electrolyte_code', 'coin_cell_code' - ]) - #立刻写入磁盘 - csvfile.flush() - #开始追加电池信息 - with open(self.csv_export_file, 'a', newline='', encoding='utf-8') as csvfile: - writer = csv.writer(csvfile) - writer.writerow([ - timestamp, data_open_circuit_voltage, data_pole_weight, - data_assembly_time, data_assembly_pressure, data_electrolyte_volume, - data_coin_num, data_electrolyte_code, data_coin_cell_code - ]) - #立刻写入磁盘 - csvfile.flush() - self.success = True - return self.success - - - - def func_pack_send_finished_cmd(self) -> bool: - """UNILAB写参数""" - while (self._unilab_rece_finished_cmd()) == False: - print("wait for rece_finished_cmd to True") - time.sleep(1) - self.success = False - self._unilab_send_finished_cmd(True) - time.sleep(1) - while (self._unilab_rece_finished_cmd()) == True: - print("wait for rece_finished_cmd to False") - time.sleep(1) - self._unilab_send_finished_cmd(False) - #将允许读取标志位置True - self.success = True - return self.success - - - - def func_allpack_cmd(self, elec_num, elec_use_num, elec_vol:int=50, assembly_type:int=7, assembly_pressure:int=4200, file_path: str="D:\\coin_cell_data") -> bool: - elec_num, elec_use_num, elec_vol, assembly_type, assembly_pressure = int(elec_num), int(elec_use_num), int(elec_vol), int(assembly_type), int(assembly_pressure) - summary_csv_file = os.path.join(file_path, "duandian.csv") - # 如果断点文件存在,先读取之前的进度 - if os.path.exists(summary_csv_file): - read_status_flag = True - with open(summary_csv_file, 'r', newline='', encoding='utf-8') as csvfile: - reader = csv.reader(csvfile) - header = next(reader) # 跳过标题行 - data_row = next(reader) # 读取数据行 - if len(data_row) >= 2: - elec_num_r = int(data_row[0]) - elec_use_num_r = int(data_row[1]) - elec_num_N = int(data_row[2]) - elec_use_num_N = int(data_row[3]) - coin_num_N = int(data_row[4]) - if elec_num_r == elec_num and elec_use_num_r == elec_use_num: - print("断点文件与当前任务匹配,继续") - else: - print("断点文件中elec_num、elec_use_num与当前任务不匹配,请检查任务下发参数或修改断点文件") - return False - print(f"从断点文件读取进度: elec_num_N={elec_num_N}, elec_use_num_N={elec_use_num_N}, coin_num_N={coin_num_N}") - - else: - read_status_flag = False - print("未找到断点文件,从头开始") - elec_num_N = 0 - elec_use_num_N = 0 - coin_num_N = 0 - for i in range(20): - print(f"剩余电解液瓶数: {elec_num}, 已组装电池数: {elec_use_num}") - print(f"剩余电解液瓶数: {type(elec_num)}, 已组装电池数: {type(elec_use_num)}") - print(f"剩余电解液瓶数: {type(int(elec_num))}, 已组装电池数: {type(int(elec_use_num))}") - - #如果是第一次运行,则进行初始化、切换自动、启动, 如果是断点重启则跳过。 - if read_status_flag == False: - pass - #初始化 - #self.func_pack_device_init() - #切换自动 - #self.func_pack_device_auto() - #启动,小车收回 - #self.func_pack_device_start() - #发送电解液瓶数量,启动搬运,多搬运没事 - #self.func_pack_send_bottle_num(elec_num) - last_i = elec_num_N - last_j = elec_use_num_N - for i in range(last_i, elec_num): - print(f"开始第{last_i+i+1}瓶电解液的组装") - #第一个循环从上次断点继续,后续循环从0开始 - j_start = last_j if i == last_i else 0 - self.func_pack_send_msg_cmd(elec_use_num-j_start, elec_vol, assembly_type, assembly_pressure) - - for j in range(j_start, elec_use_num): - print(f"开始第{last_i+i+1}瓶电解液的第{j+j_start+1}个电池组装") - #读取电池组装数据并存入csv - self.func_pack_get_msg_cmd(file_path) - time.sleep(1) - # TODO:读完再将电池数加一还是进入循环就将电池数加一需要考虑 - - - - # 生成断点文件 - # 生成包含elec_num_N、coin_num_N、timestamp的CSV文件 - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - with open(summary_csv_file, 'w', newline='', encoding='utf-8') as csvfile: - writer = csv.writer(csvfile) - writer.writerow(['elec_num','elec_use_num', 'elec_num_N', 'elec_use_num_N', 'coin_num_N', 'timestamp']) - writer.writerow([elec_num, elec_use_num, elec_num_N, elec_use_num_N, coin_num_N, timestamp]) - csvfile.flush() - coin_num_N += 1 - self.coin_num_N = coin_num_N - elec_use_num_N += 1 - elec_num_N += 1 - elec_use_num_N = 0 - - #循环正常结束,则删除断点文件 - os.remove(summary_csv_file) - #全部完成后等待依华发送完成信号 - self.func_pack_send_finished_cmd() - - - def func_pack_device_stop(self) -> bool: - """打包指令:设备停止""" - for i in range(3): - time.sleep(2) - print(f"输出{i}") - #print("_sys_hand_cmd", self._sys_hand_cmd()) - #time.sleep(1) - #print("_sys_hand_status", self._sys_hand_status()) - #time.sleep(1) - #print("_sys_init_cmd", self._sys_init_cmd()) - #time.sleep(1) - #print("_sys_init_status", self._sys_init_status()) - #time.sleep(1) - #print("_sys_auto_status", self._sys_auto_status()) - #time.sleep(1) - #print("data_axis_y_pos", self.data_axis_y_pos) - #time.sleep(1) - #self.success = False - #with open('action_device_stop.json', 'r', encoding='utf-8') as f: - # action_json = json.load(f) - #self.client.execute_procedure_from_json(action_json) - #self.success = True - #return self.success - - def fun_wuliao_test(self) -> bool: - #找到data_init中构建的2个物料盘 - liaopan3 = self.station_resource.get_resource("\u7535\u6c60\u6599\u76d8") - for i in range(16): - battery = ElectrodeSheet(name=f"battery_{i}", size_x=16, size_y=16, size_z=2) - battery._unilabos_state = { - "diameter": 20.0, - "height": 20.0, - "assembly_pressure": i, - "electrolyte_volume": 20.0, - "electrolyte_name": f"DP{i}" - } - liaopan3.children[i].assign_child_resource(battery, location=None) - - ROS2DeviceNode.run_async_func(self._ros_node.update_resource, True, **{ - "resources": [self.station_resource] - }) - time.sleep(4) - # 数据读取与输出 - def func_read_data_and_output(self, file_path: str="D:\\coin_cell_data"): - # 检查CSV导出是否正在运行,已运行则跳出,防止同时启动两个while循环 - if self.csv_export_running: - return False, "读取已在运行中" - - #若不存在该目录则创建 - if not os.path.exists(file_path): - os.makedirs(file_path) - print(f"创建目录: {file_path}") - - # 只要允许读取标志位为true,就持续运行该函数,直到触发停止条件 - while self.allow_data_read: - - #函数运行标志位,确保只同时启动一个导出函数 - self.csv_export_running = True - - #等待接收结果标志位置True - while self.request_send_msg_status == False: - print("waiting for send_msg_status to True") - time.sleep(1) - #日期时间戳用于按天存放csv文件 - time_date = datetime.now().strftime("%Y%m%d") - #秒级时间戳用于标记每一行电池数据 - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - #生成输出文件的变量 - self.csv_export_file = os.path.join(file_path, f"date_{time_date}.csv") - - #接收信息 - data_open_circuit_voltage = self.data_open_circuit_voltage - data_pole_weight = self.data_pole_weight - data_assembly_time = self.data_assembly_time - data_assembly_pressure = self.data_assembly_pressure - data_electrolyte_volume = self.data_electrolyte_volume - data_coin_num = self.data_coin_num - data_electrolyte_code = self.data_electrolyte_code - data_coin_cell_code = self.data_coin_cell_code - # 电解液瓶位置 - elec_bottle_site = 2 - # 极片夹取位置(应当通过寄存器读光标) - Pos_elec_site = 0 - Al_elec_site = 0 - Gasket_site = 0 - - #接收完信息后,读取完毕标志位置True - self._unilab_rec_msg_succ_cmd()# = True - #等待允许读取标志位置False - while self.request_send_msg_status == True: - print("waiting for send_msg_status to False") - time.sleep(1) - self._unilab_rec_msg_succ_cmd()# = False - - #此处操作物料信息(如果中途报错停止,如何) - #报错怎么办(加个判断标志位,如果发生错误,则根据停止位置扣除物料) - #根据物料光标判断取哪个物料(人工摆盘,电解液瓶,移液枪头都有光标位置,寄存器读即可) - - #物料读取操作写在这里 - #在这里进行物料调取 - #转移物料瓶,elec_bottle_site对应第几瓶电解液(从依华寄存器读取) - # transfer_bottle(deck, elec_bottle_site) - # #找到电解液瓶的对象 - # electrolyte_rack = deck.get_resource("electrolyte_rack") - # pending_positions = electrolyte_rack.get_pending_positions()[elec_bottle_site] - # # TODO: 瓶子取液体操作需要加入 -# -# - # #找到压制工站对应的对象 - # battery_press_slot = deck.get_resource("battery_press_1") - # #创建一个新电池 - # test_battery = Battery( - # name=f"test_battery_{data_coin_num}", - # diameter=20.0, # 与压制槽直径匹配 - # height=3.0, # 电池高度 - # max_volume=100.0, # 100μL容量 - # barcode=data_coin_cell_code, # 电池条码 - # ) - # if battery_press_slot.has_battery(): - # return False, "压制工站已有电池,无法放置新电池" - # #在压制位放置电池 - # battery_press_slot.place_battery(test_battery) - # #从第一个子弹夹中取料 - # clip_magazine_1_hole = self.deck.get_resource("clip_magazine_1").get_item(Pos_elec_site) - # clip_magazine_2_hole = self.deck.get_resource("clip_magazine_2").get_item(Al_elec_site) - # clip_magazine_3_hole = self.deck.get_resource("clip_magazine_3").get_item(Gasket_site) - # - # if clip_magazine_1_hole.get_sheet_count() > 0: # 检查洞位是否有极片 - # electrode_sheet_1 = clip_magazine_1_hole.take_sheet() # 从洞位取出极片 - # test_battery.add_electrode_sheet(electrode_sheet_1) # 添加到电池中 - # print(f"已将极片 {electrode_sheet_1.name} 从子弹夹转移到电池") - # else: - # print("子弹夹洞位0没有极片") -# - # if clip_magazine_2_hole.get_sheet_count() > 0: # 检查洞位是否有极片 - # electrode_sheet_2 = clip_magazine_2_hole.take_sheet() # 从洞位取出极片 - # test_battery.add_electrode_sheet(electrode_sheet_2) # 添加到电池中 - # print(f"已将极片 {electrode_sheet_2.name} 从子弹夹转移到电池") - # else: - # print("子弹夹洞位0没有极片") -# - # if clip_magazine_3_hole.get_sheet_count() > 0: # 检查洞位是否有极片 - # electrode_sheet_3 = clip_magazine_3_hole.take_sheet() # 从洞位取出极片 - # test_battery.add_electrode_sheet(electrode_sheet_3) # 添加到电池中 - # print(f"已将极片 {electrode_sheet_3.name} 从子弹夹转移到电池") - # else: - # print("子弹夹洞位0没有极片") - # - # # TODO:#把电解液从瓶中取到电池夹子中 - # battery_site = deck.get_resource("battery_press_1") - # clip_magazine_battery = deck.get_resource("clip_magazine_battery") - # if battery_site.has_battery(): - # battery = battery_site.take_battery() #从压制槽取出电池 - # clip_magazine_battery.add_battery(battery) #从压制槽取出电池 -# -# -# -# - # # 保存配置到文件 - # self.deck.save("button_battery_station_layout.json", indent=2) - # print("\n台面配置已保存到: button_battery_station_layout.json") - # - # # 保存状态到文件 - # self.deck.save_state_to_file("button_battery_station_state.json", indent=2) - # print("台面状态已保存到: button_battery_station_state.json") - - - - - - - #将数据写入csv中 - #如当前目录下无同名文件则新建一个csv用于存放数据 - if not os.path.exists(self.csv_export_file): - #创建一个表头 - with open(self.csv_export_file, 'w', newline='', encoding='utf-8') as csvfile: - writer = csv.writer(csvfile) - writer.writerow([ - 'Time', 'open_circuit_voltage', 'pole_weight', - 'assembly_time', 'assembly_pressure', 'electrolyte_volume', - 'coin_num', 'electrolyte_code', 'coin_cell_code' - ]) - #立刻写入磁盘 - csvfile.flush() - #开始追加电池信息 - with open(self.csv_export_file, 'a', newline='', encoding='utf-8') as csvfile: - writer = csv.writer(csvfile) - writer.writerow([ - timestamp, data_open_circuit_voltage, data_pole_weight, - data_assembly_time, data_assembly_pressure, data_electrolyte_volume, - data_coin_num, data_electrolyte_code, data_coin_cell_code - ]) - #立刻写入磁盘 - csvfile.flush() - - # 只要不在自动模式运行中,就将允许标志位置False - if self.sys_auto_status == False or self.sys_start_status == False: - self.allow_data_read = False - self.csv_export_running = False - time.sleep(1) - - def func_stop_read_data(self): - """停止CSV导出""" - if not self.csv_export_running: - return False, "read data未在运行" - - self.csv_export_running = False - self.allow_data_read = False - - if self.csv_export_thread and self.csv_export_thread.is_alive(): - self.csv_export_thread.join(timeout=5) - - def func_get_csv_export_status(self): - """获取CSV导出状态""" - return { - 'allow_read': self.allow_data_read, - 'running': self.csv_export_running, - 'thread_alive': self.csv_export_thread.is_alive() if self.csv_export_thread else False - } - - - ''' - # ===================== 物料管理区 ====================== - @property - def data_material_inventory(self) -> int: - """主物料库存 (数量, INT16)""" - inventory, read_err = self.client.use_node('REG_DATA_MATERIAL_INVENTORY').read(1) - return inventory - - @property - def data_tips_inventory(self) -> int: - """移液枪头库存 (数量, INT16)""" - inventory, read_err = self.client.register_node_list(self.nodes).use_node('REG_DATA_TIPS_INVENTORY').read(1) - return inventory - - ''' - - -if __name__ == "__main__": - from pylabrobot.resources import Resource - Coin_Cell = CoinCellAssemblyWorkstation(Resource("1", 1, 1, 1), debug_mode=True) - #Coin_Cell.func_pack_device_init() - #Coin_Cell.func_pack_device_auto() - #Coin_Cell.func_pack_device_start() - #Coin_Cell.func_pack_send_bottle_num(2) - #Coin_Cell.func_pack_send_msg_cmd(2) - #Coin_Cell.func_pack_get_msg_cmd() - #Coin_Cell.func_pack_get_msg_cmd() - #Coin_Cell.func_pack_send_finished_cmd() -# - #Coin_Cell.func_allpack_cmd(3, 2) - #print(Coin_Cell.data_stack_vision_code) - #print("success") - #创建一个物料台面 - - deck = create_a_coin_cell_deck() - #deck = create_a_full_coin_cell_deck() - - - ##在台面上找到料盘和极片 - #liaopan1 = deck.get_resource("liaopan1") - #liaopan2 = deck.get_resource("liaopan2") - #jipian1 = liaopan1.children[1].children[0] -## - #print(jipian1) - ##把物料解绑后放到另一盘上 - #jipian1.parent.unassign_child_resource(jipian1) - #liaopan2.children[1].assign_child_resource(jipian1, location=None) - ##print(jipian2.parent) - - liaopan1 = deck.get_resource("liaopan1") - liaopan2 = deck.get_resource("liaopan2") - for i in range(16): - #找到liaopan1上每一个jipian - jipian_linshi = liaopan1.children[i].children[0] - #把物料解绑后放到另一盘上 - print("极片:", jipian_linshi) - jipian_linshi.parent.unassign_child_resource(jipian_linshi) - liaopan2.children[i].assign_child_resource(jipian_linshi, location=None) - - - from unilabos.resources.graphio import resource_ulab_to_plr, convert_resources_to_type - #with open("./button_battery_station_resources_unilab.json", "r", encoding="utf-8") as f: - # bioyond_resources_unilab = json.load(f) - #print(f"成功读取 JSON 文件,包含 {len(bioyond_resources_unilab)} 个资源") - #ulab_resources = convert_resources_to_type(bioyond_resources_unilab, List[PLRResource]) - #print(f"转换结果类型: {type(ulab_resources)}") - #print(ulab_resources) - - - - from unilabos.resources.graphio import convert_resources_from_type - from unilabos.config.config import BasicConfig - BasicConfig.ak = "beb0c15f-2279-46a1-aba5-00eaf89aef55" - BasicConfig.sk = "15d4f25e-3512-4f9c-9bfb-43ab85e7b561" - from unilabos.app.web.client import http_client - - resources = convert_resources_from_type([deck], [Resource]) - json.dump({"nodes": resources, "links": []}, open("button_battery_station_resources_unilab.json", "w"), indent=2) - - #print(resources) - http_client.remote_addr = "https://uni-lab.test.bohrium.com/api/v1" - - http_client.resource_add(resources) \ No newline at end of file diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/new_cellconfig3c.json b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/new_cellconfig3c.json deleted file mode 100644 index 630faa58..00000000 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/new_cellconfig3c.json +++ /dev/null @@ -1,691 +0,0 @@ -{ - "nodes": [ - { - "id": "BatteryStation", - "name": "扣电工作站", - "children": [ - "coin_cell_deck" - ], - "parent": null, - "type": "device", - "class": "bettery_station_registry", - "position": { - "x": 600, - "y": 400, - "z": 0 - }, - "config": { - "debug_mode": false, - "_comment": "protocol_type接外部工站固定写法字段,一般为空,station_resource写法也固定", - "protocol_type": [], - "station_resource": { - "data": { - "_resource_child_name": "coin_cell_deck", - "_resource_type": "unilabos.devices.workstation.coin_cell_assembly.button_battery_station:CoincellDeck" - } - }, - - "address": "192.168.1.20", - "port": 502 - }, - "data": {} - }, - { - "id": "coin_cell_deck", - "name": "coin_cell_deck", - "sample_id": null, - "children": [ - "\u7535\u6c60\u6599\u76d8" - ], - "parent": null, - "type": "container", - "class": "", - "position": { - "x": 0, - "y": 0, - "z": 0 - }, - "config": { - "type": "CoincellDeck", - "size_x": 1000, - "size_y": 1000, - "size_z": 900, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "coin_cell_deck", - "barcode": null - }, - "data": {} - }, - { - "id": "\u7535\u6c60\u6599\u76d8", - "name": "\u7535\u6c60\u6599\u76d8", - "sample_id": null, - "children": [ - "\u7535\u6c60\u6599\u76d8_materialhole_0_0", - "\u7535\u6c60\u6599\u76d8_materialhole_0_1", - "\u7535\u6c60\u6599\u76d8_materialhole_0_2", - "\u7535\u6c60\u6599\u76d8_materialhole_0_3", - "\u7535\u6c60\u6599\u76d8_materialhole_1_0", - "\u7535\u6c60\u6599\u76d8_materialhole_1_1", - "\u7535\u6c60\u6599\u76d8_materialhole_1_2", - "\u7535\u6c60\u6599\u76d8_materialhole_1_3", - "\u7535\u6c60\u6599\u76d8_materialhole_2_0", - "\u7535\u6c60\u6599\u76d8_materialhole_2_1", - "\u7535\u6c60\u6599\u76d8_materialhole_2_2", - "\u7535\u6c60\u6599\u76d8_materialhole_2_3", - "\u7535\u6c60\u6599\u76d8_materialhole_3_0", - "\u7535\u6c60\u6599\u76d8_materialhole_3_1", - "\u7535\u6c60\u6599\u76d8_materialhole_3_2", - "\u7535\u6c60\u6599\u76d8_materialhole_3_3" - ], - "parent": "coin_cell_deck", - "type": "container", - "class": "", - "position": { - "x": 100, - "y": 100, - "z": 0 - }, - "config": { - "type": "MaterialPlate", - "size_x": 120.8, - "size_y": 160.5, - "size_z": 10.0, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_plate", - "model": null, - "barcode": null, - "ordering": { - "A1": "\u7535\u6c60\u6599\u76d8_materialhole_0_0", - "B1": "\u7535\u6c60\u6599\u76d8_materialhole_0_1", - "C1": "\u7535\u6c60\u6599\u76d8_materialhole_0_2", - "D1": "\u7535\u6c60\u6599\u76d8_materialhole_0_3", - "A2": "\u7535\u6c60\u6599\u76d8_materialhole_1_0", - "B2": "\u7535\u6c60\u6599\u76d8_materialhole_1_1", - "C2": "\u7535\u6c60\u6599\u76d8_materialhole_1_2", - "D2": "\u7535\u6c60\u6599\u76d8_materialhole_1_3", - "A3": "\u7535\u6c60\u6599\u76d8_materialhole_2_0", - "B3": "\u7535\u6c60\u6599\u76d8_materialhole_2_1", - "C3": "\u7535\u6c60\u6599\u76d8_materialhole_2_2", - "D3": "\u7535\u6c60\u6599\u76d8_materialhole_2_3", - "A4": "\u7535\u6c60\u6599\u76d8_materialhole_3_0", - "B4": "\u7535\u6c60\u6599\u76d8_materialhole_3_1", - "C4": "\u7535\u6c60\u6599\u76d8_materialhole_3_2", - "D4": "\u7535\u6c60\u6599\u76d8_materialhole_3_3" - } - }, - "data": {} - }, - { - "id": "\u7535\u6c60\u6599\u76d8_materialhole_0_0", - "name": "\u7535\u6c60\u6599\u76d8_materialhole_0_0", - "sample_id": null, - "children": [], - "parent": "\u7535\u6c60\u6599\u76d8", - "type": "container", - "class": "", - "position": { - "x": 12.4, - "y": 104.25, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "\u7535\u6c60\u6599\u76d8_materialhole_0_1", - "name": "\u7535\u6c60\u6599\u76d8_materialhole_0_1", - "sample_id": null, - "children": [], - "parent": "\u7535\u6c60\u6599\u76d8", - "type": "container", - "class": "", - "position": { - "x": 12.4, - "y": 80.25, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "\u7535\u6c60\u6599\u76d8_materialhole_0_2", - "name": "\u7535\u6c60\u6599\u76d8_materialhole_0_2", - "sample_id": null, - "children": [], - "parent": "\u7535\u6c60\u6599\u76d8", - "type": "container", - "class": "", - "position": { - "x": 12.4, - "y": 56.25, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "\u7535\u6c60\u6599\u76d8_materialhole_0_3", - "name": "\u7535\u6c60\u6599\u76d8_materialhole_0_3", - "sample_id": null, - "children": [], - "parent": "\u7535\u6c60\u6599\u76d8", - "type": "container", - "class": "", - "position": { - "x": 12.4, - "y": 32.25, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "\u7535\u6c60\u6599\u76d8_materialhole_1_0", - "name": "\u7535\u6c60\u6599\u76d8_materialhole_1_0", - "sample_id": null, - "children": [], - "parent": "\u7535\u6c60\u6599\u76d8", - "type": "container", - "class": "", - "position": { - "x": 36.4, - "y": 104.25, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "\u7535\u6c60\u6599\u76d8_materialhole_1_1", - "name": "\u7535\u6c60\u6599\u76d8_materialhole_1_1", - "sample_id": null, - "children": [], - "parent": "\u7535\u6c60\u6599\u76d8", - "type": "container", - "class": "", - "position": { - "x": 36.4, - "y": 80.25, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "\u7535\u6c60\u6599\u76d8_materialhole_1_2", - "name": "\u7535\u6c60\u6599\u76d8_materialhole_1_2", - "sample_id": null, - "children": [], - "parent": "\u7535\u6c60\u6599\u76d8", - "type": "container", - "class": "", - "position": { - "x": 36.4, - "y": 56.25, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "\u7535\u6c60\u6599\u76d8_materialhole_1_3", - "name": "\u7535\u6c60\u6599\u76d8_materialhole_1_3", - "sample_id": null, - "children": [], - "parent": "\u7535\u6c60\u6599\u76d8", - "type": "container", - "class": "", - "position": { - "x": 36.4, - "y": 32.25, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "\u7535\u6c60\u6599\u76d8_materialhole_2_0", - "name": "\u7535\u6c60\u6599\u76d8_materialhole_2_0", - "sample_id": null, - "children": [], - "parent": "\u7535\u6c60\u6599\u76d8", - "type": "container", - "class": "", - "position": { - "x": 60.4, - "y": 104.25, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "\u7535\u6c60\u6599\u76d8_materialhole_2_1", - "name": "\u7535\u6c60\u6599\u76d8_materialhole_2_1", - "sample_id": null, - "children": [], - "parent": "\u7535\u6c60\u6599\u76d8", - "type": "container", - "class": "", - "position": { - "x": 60.4, - "y": 80.25, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "\u7535\u6c60\u6599\u76d8_materialhole_2_2", - "name": "\u7535\u6c60\u6599\u76d8_materialhole_2_2", - "sample_id": null, - "children": [], - "parent": "\u7535\u6c60\u6599\u76d8", - "type": "container", - "class": "", - "position": { - "x": 60.4, - "y": 56.25, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "\u7535\u6c60\u6599\u76d8_materialhole_2_3", - "name": "\u7535\u6c60\u6599\u76d8_materialhole_2_3", - "sample_id": null, - "children": [], - "parent": "\u7535\u6c60\u6599\u76d8", - "type": "container", - "class": "", - "position": { - "x": 60.4, - "y": 32.25, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "\u7535\u6c60\u6599\u76d8_materialhole_3_0", - "name": "\u7535\u6c60\u6599\u76d8_materialhole_3_0", - "sample_id": null, - "children": [], - "parent": "\u7535\u6c60\u6599\u76d8", - "type": "container", - "class": "", - "position": { - "x": 84.4, - "y": 104.25, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "\u7535\u6c60\u6599\u76d8_materialhole_3_1", - "name": "\u7535\u6c60\u6599\u76d8_materialhole_3_1", - "sample_id": null, - "children": [], - "parent": "\u7535\u6c60\u6599\u76d8", - "type": "container", - "class": "", - "position": { - "x": 84.4, - "y": 80.25, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "\u7535\u6c60\u6599\u76d8_materialhole_3_2", - "name": "\u7535\u6c60\u6599\u76d8_materialhole_3_2", - "sample_id": null, - "children": [], - "parent": "\u7535\u6c60\u6599\u76d8", - "type": "container", - "class": "", - "position": { - "x": 84.4, - "y": 56.25, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - }, - { - "id": "\u7535\u6c60\u6599\u76d8_materialhole_3_3", - "name": "\u7535\u6c60\u6599\u76d8_materialhole_3_3", - "sample_id": null, - "children": [], - "parent": "\u7535\u6c60\u6599\u76d8", - "type": "container", - "class": "", - "position": { - "x": 84.4, - "y": 32.25, - "z": 10.0 - }, - "config": { - "type": "MaterialHole", - "size_x": 16, - "size_y": 16, - "size_z": 16, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "material_hole", - "model": null, - "barcode": null - }, - "data": { - "diameter": 20, - "depth": 10, - "max_sheets": 1, - "info": null - } - } - ], - "links": [] -} \ No newline at end of file diff --git a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/coin_cell_assembly_a.csv b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_a.csv similarity index 67% rename from unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/coin_cell_assembly_a.csv rename to unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_a.csv index 836fb712..bb66a7a7 100644 --- a/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly/coin_cell_assembly_a.csv +++ b/unilabos/devices/workstation/coin_cell_assembly/coin_cell_assembly_a.csv @@ -43,21 +43,21 @@ REG_DATA_ELECTROLYTE_USE_NUM,INT16,,,,hold_register,10000, UNILAB_SEND_FINISHED_CMD,BOOL,,,,coil,8730, UNILAB_RECE_FINISHED_CMD,BOOL,,,,coil,8530, REG_DATA_ASSEMBLY_TYPE,INT16,,,,hold_register,10018,ASSEMBLY_TYPE7or8 -COIL_ALUMINUM_FOIL,BOOL,,ʹ,,coil,8340, -REG_MSG_NE_PLATE_MATRIX,INT16,,Ƭλ,,hold_register,440, -REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,Ĥλ,,hold_register,450, -REG_MSG_TIP_BOX_MATRIX,INT16,,Һǹͷλ,,hold_register,480, -REG_MSG_NE_PLATE_NUM,INT16,,Ƭ,,hold_register,443, -REG_MSG_SEPARATOR_PLATE_NUM,INT16,,Ĥ,,hold_register,453, -REG_MSG_PRESS_MODE,BOOL,,ѹģʽfalse:ѹģʽTrue:ģʽ,,coil,8360,ѹģʽ +COIL_ALUMINUM_FOIL,BOOL,,,,coil,8340, +REG_MSG_NE_PLATE_MATRIX,INT16,,负极片矩阵点位,,hold_register,440, +REG_MSG_SEPARATOR_PLATE_MATRIX,INT16,,隔膜矩阵点位,,hold_register,450, +REG_MSG_TIP_BOX_MATRIX,INT16,,移液枪头矩阵点位,,hold_register,480, +REG_MSG_NE_PLATE_NUM,INT16,,负极片盘数,,hold_register,443, +REG_MSG_SEPARATOR_PLATE_NUM,INT16,,隔膜盘数,,hold_register,453, ,,,,,,, -,BOOL,,Ӿλfalse:ʹãtrue:ԣ,,coil,8300,Ӿλ -,BOOL,,죨false:ʹãtrue:ԣ,,coil,8310,Ӿ -,BOOL,,_֣false:ʹãtrue:ԣ,,coil,8320, -,BOOL,,_Ҳ֣false:ʹãtrue:ԣ,,coil,8420,Ҳ -,BOOL,,ռ֪false:ʹãtrue:ԣ,,coil,8350,ռ֪ -,BOOL,,Һģʽfalse:εҺtrue:εҺ,,coil,8370,Һģʽ -,BOOL,,Ƭأfalse:ʹãtrue:ԣ,,coil,8380,Ƭ -,BOOL,,Ƭװʽfalse:װtrue:װ,,coil,8390,װ -,BOOL,,ѹࣨfalse:ʹãtrue:ԣ,,coil,8400,ѹ -,BOOL,,̷̰ʽfalse:ˮƽ̣true:ѵ̣,,coil,8410,Ƭ̷ʽ +,BOOL,,视觉对位(false:使用,true:忽略),,coil,8300,视觉对位 +,BOOL,,复检(false:使用,true:忽略),,coil,8310,视觉复检 +,BOOL,,手套箱_左仓(false:使用,true:忽略),,coil,8320,手套箱左仓 +,BOOL,,手套箱_右仓(false:使用,true:忽略),,coil,8420,手套箱右仓 +,BOOL,,真空检知(false:使用,true:忽略),,coil,8350,真空检知 +,BOOL,,压制模式(false:压力检测模式,True:距离模式),,coil,8360,电池压制模式 +,BOOL,,电解液添加模式(false:单次滴液,true:二次滴液),,coil,8370,滴液模式 +,BOOL,,正极片称重(false:使用,true:忽略),,coil,8380,正极片称重 +,BOOL,,正负极片组装方式(false:正装,true:倒装),,coil,8390,正负极反装 +,BOOL,,压制清洁(false:使用,true:忽略),,coil,8400,压制清洁 +,BOOL,,物料盘摆盘方式(false:水平摆盘,true:堆叠摆盘),,coil,8410,负极片摆盘方式 diff --git a/unilabos/devices/workstation/coin_cell_assembly/new_cellconfig3c.json b/unilabos/devices/workstation/coin_cell_assembly/new_cellconfig3c.json new file mode 100644 index 00000000..c96e6b2b --- /dev/null +++ b/unilabos/devices/workstation/coin_cell_assembly/new_cellconfig3c.json @@ -0,0 +1,38 @@ +{ + "nodes": [ + { + "id": "bioyond_cell_workstation", + "name": "配液分液工站", + "children": [ + ], + "parent": null, + "type": "device", + "class": "bioyond_cell", + "config": { + "protocol_type": [], + "station_resource": {} + }, + "data": {} + }, + { + "id": "BatteryStation", + "name": "扣电工作站", + "children": [ + "coin_cell_deck" + ], + "parent": null, + "type": "device", + "class": "bettery_station_registry", + "position": { + "x": 600, + "y": 400, + "z": 0 + }, + "config": { + "debug_mode": false, + "protocol_type": [] + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/unilabos/devices/workstation/coin_cell_assembly/workstation_base.py b/unilabos/devices/workstation/coin_cell_assembly/workstation_base.py deleted file mode 100644 index 65b34a93..00000000 --- a/unilabos/devices/workstation/coin_cell_assembly/workstation_base.py +++ /dev/null @@ -1,489 +0,0 @@ -""" -工作站基类 -Workstation Base Class - 简化版 - -基于PLR Deck的简化工作站架构 -专注于核心物料系统和工作流管理 -""" - -import collections -import time -from typing import Dict, Any, List, Optional, Union -from abc import ABC, abstractmethod -from dataclasses import dataclass -from enum import Enum -from pylabrobot.resources import Deck, Plate, Resource as PLRResource - -from pylabrobot.resources.coordinate import Coordinate -from unilabos.ros.nodes.presets.workstation import ROS2WorkstationNode - -from unilabos.utils.log import logger - - -class WorkflowStatus(Enum): - """工作流状态""" - - IDLE = "idle" - INITIALIZING = "initializing" - RUNNING = "running" - PAUSED = "paused" - STOPPING = "stopping" - STOPPED = "stopped" - ERROR = "error" - COMPLETED = "completed" - - -@dataclass -class WorkflowInfo: - """工作流信息""" - - name: str - description: str - estimated_duration: float # 预估持续时间(秒) - required_materials: List[str] # 所需物料类型 - output_product: str # 输出产品类型 - parameters_schema: Dict[str, Any] # 参数架构 - - -class WorkStationContainer(Plate): - """ - WorkStation 专用 Container 类,继承自 Plate和TipRack - 注意这个物料必须通过plr_additional_res_reg.py注册到edge,才能正常序列化 - """ - - def __init__( - self, - name: str, - size_x: float, - size_y: float, - size_z: float, - category: str, - ordering: collections.OrderedDict, - model: Optional[str] = None, - ): - """ - 这里的初始化入参要和plr的保持一致 - """ - super().__init__(name, size_x, size_y, size_z, category=category, ordering=ordering, model=model) - self._unilabos_state = {} # 必须有此行,自己的类描述的是物料的 - - def load_state(self, state: Dict[str, Any]) -> None: - """从给定的状态加载工作台信息。""" - super().load_state(state) - self._unilabos_state = state - - def serialize_state(self) -> Dict[str, Dict[str, Any]]: - data = super().serialize_state() - data.update( - self._unilabos_state - ) # Container自身的信息,云端物料将保存这一data,本地也通过这里的data进行读写,当前类用来表示这个物料的长宽高大小的属性,而data(state用来表示物料的内容,细节等) - return data - - -def get_workstation_plate_resource(name: str) -> PLRResource: # 要给定一个返回plr的方法 - """ - 用于获取一些模板,例如返回一个带有特定信息/子物料的 Plate,这里需要到注册表注册,例如unilabos/registry/resources/organic/workstation.yaml - 可以直接运行该函数或者利用注册表补全机制,来检查是否资源出错 - :param name: 资源名称 - :return: Resource对象 - """ - plate = WorkStationContainer( - name, size_x=50, size_y=50, size_z=10, category="plate", ordering=collections.OrderedDict() - ) - tip_rack = WorkStationContainer( - "tip_rack_inside_plate", - size_x=50, - size_y=50, - size_z=10, - category="tip_rack", - ordering=collections.OrderedDict(), - ) - plate.assign_child_resource(tip_rack, Coordinate.zero()) - return plate - - -class ResourceSynchronizer(ABC): - """资源同步器基类 - - 负责与外部物料系统的同步,并对 self.deck 做修改 - """ - - def __init__(self, workstation: "WorkstationBase"): - self.workstation = workstation - - @abstractmethod - async def sync_from_external(self) -> bool: - """从外部系统同步物料到本地deck""" - pass - - @abstractmethod - async def sync_to_external(self, plr_resource: PLRResource) -> bool: - """将本地物料同步到外部系统""" - pass - - @abstractmethod - async def handle_external_change(self, change_info: Dict[str, Any]) -> bool: - """处理外部系统的变更通知""" - pass - - -class WorkstationBase(ABC): - """工作站基类 - 简化版 - - 核心功能: - 1. 基于 PLR Deck 的物料系统,支持格式转换 - 2. 可选的资源同步器支持外部物料系统 - 3. 简化的工作流管理 - """ - - _ros_node: ROS2WorkstationNode - - @property - def _children(self) -> Dict[str, Any]: # 不要删除这个下划线,不然会自动导入注册表,后面改成装饰器识别 - return self._ros_node.children - - async def update_resource_example(self): - return await self._ros_node.update_resource([get_workstation_plate_resource("test")]) - - def __init__( - self, - station_resource: PLRResource, - *args, - **kwargs, # 必须有kwargs - ): - # 基本配置 - print(station_resource) - self.deck_config = station_resource - - # PLR 物料系统 - self.deck: Optional[Deck] = None - self.plr_resources: Dict[str, PLRResource] = {} - - # 资源同步器(可选) - # self.resource_synchronizer = ResourceSynchronizer(self) # 要在driver中自行初始化,只有workstation用 - - # 硬件接口 - self.hardware_interface: Union[Any, str] = None - - # 工作流状态 - self.current_workflow_status = WorkflowStatus.IDLE - self.current_workflow_info = None - self.workflow_start_time = None - self.workflow_parameters = {} - - # 支持的工作流(静态预定义) - self.supported_workflows: Dict[str, WorkflowInfo] = {} - - # 初始化物料系统 - self._initialize_material_system() - - # 注册支持的工作流 - # self._register_supported_workflows() - - # logger.info(f"工作站 {device_id} 初始化完成(简化版)") - - def _initialize_material_system(self): - """初始化物料系统 - 使用 graphio 转换""" - try: - from unilabos.resources.graphio import resource_ulab_to_plr - - # # 1. 合并 deck_config 和 children 创建完整的资源树 - # complete_resource_config = self._create_complete_resource_config() - - # # 2. 使用 graphio 转换为 PLR 资源 - # self.deck = resource_ulab_to_plr(complete_resource_config, plr_model=True) - - # # 3. 建立资源映射 - # self._build_resource_mappings(self.deck) - - # # 4. 如果有资源同步器,执行初始同步 - # if self.resource_synchronizer: - # # 这里可以异步执行,暂时跳过 - # pass - - # logger.info(f"工作站 {self.device_id} 物料系统初始化成功,创建了 {len(self.plr_resources)} 个资源") - pass - except Exception as e: - # logger.error(f"工作站 {self.device_id} 物料系统初始化失败: {e}") - raise - - def _create_complete_resource_config(self) -> Dict[str, Any]: - """创建完整的资源配置 - 合并 deck_config 和 children""" - # 创建主 deck 配置 - deck_resource = { - "id": f"{self.device_id}_deck", - "name": f"{self.device_id}_deck", - "type": "deck", - "position": {"x": 0, "y": 0, "z": 0}, - "config": { - "size_x": self.deck_config.get("size_x", 1000.0), - "size_y": self.deck_config.get("size_y", 1000.0), - "size_z": self.deck_config.get("size_z", 100.0), - **{k: v for k, v in self.deck_config.items() if k not in ["size_x", "size_y", "size_z"]}, - }, - "data": {}, - "children": [], - "parent": None, - } - - # 添加子资源 - if self._children: - children_list = [] - for child_id, child_config in self._children.items(): - child_resource = self._normalize_child_resource(child_id, child_config, deck_resource["id"]) - children_list.append(child_resource) - deck_resource["children"] = children_list - - return deck_resource - - def _normalize_child_resource(self, resource_id: str, config: Dict[str, Any], parent_id: str) -> Dict[str, Any]: - """标准化子资源配置""" - return { - "id": resource_id, - "name": config.get("name", resource_id), - "type": config.get("type", "container"), - "position": self._normalize_position(config.get("position", {})), - "config": config.get("config", {}), - "data": config.get("data", {}), - "children": [], # 简化版本:只支持一层子资源 - "parent": parent_id, - } - - def _normalize_position(self, position: Any) -> Dict[str, float]: - """标准化位置信息""" - if isinstance(position, dict): - return { - "x": float(position.get("x", 0)), - "y": float(position.get("y", 0)), - "z": float(position.get("z", 0)), - } - elif isinstance(position, (list, tuple)) and len(position) >= 2: - return { - "x": float(position[0]), - "y": float(position[1]), - "z": float(position[2]) if len(position) > 2 else 0.0, - } - else: - return {"x": 0.0, "y": 0.0, "z": 0.0} - - def _build_resource_mappings(self, deck: Deck): - """递归构建资源映射""" - - def add_resource_recursive(resource: PLRResource): - if hasattr(resource, "name"): - self.plr_resources[resource.name] = resource - - if hasattr(resource, "children"): - for child in resource.children: - add_resource_recursive(child) - - add_resource_recursive(deck) - - # ============ 硬件接口管理 ============ - - def set_hardware_interface(self, hardware_interface: Union[Any, str]): - """设置硬件接口""" - self.hardware_interface = hardware_interface - logger.info(f"工作站 {self.device_id} 硬件接口设置: {type(hardware_interface).__name__}") - - def set_workstation_node(self, workstation_node: "ROS2WorkstationNode"): - """设置协议节点引用(用于代理模式)""" - self._ros_node = workstation_node - logger.info(f"工作站 {self.device_id} 关联协议节点") - - # ============ 设备操作接口 ============ - - def call_device_method(self, method: str, *args, **kwargs) -> Any: - """调用设备方法的统一接口""" - # 1. 代理模式:通过协议节点转发 - if isinstance(self.hardware_interface, str) and self.hardware_interface.startswith("proxy:"): - if not self._ros_node: - raise RuntimeError("代理模式需要设置workstation_node") - - device_id = self.hardware_interface[6:] # 移除 "proxy:" 前缀 - return self._ros_node.call_device_method(device_id, method, *args, **kwargs) - - # 2. 直接模式:直接调用硬件接口方法 - elif self.hardware_interface and hasattr(self.hardware_interface, method): - return getattr(self.hardware_interface, method)(*args, **kwargs) - - else: - raise AttributeError(f"硬件接口不支持方法: {method}") - - def get_device_status(self) -> Dict[str, Any]: - """获取设备状态""" - try: - return self.call_device_method("get_status") - except AttributeError: - # 如果设备不支持get_status方法,返回基础状态 - return { - "status": "unknown", - "interface_type": type(self.hardware_interface).__name__, - "timestamp": time.time(), - } - - def is_device_available(self) -> bool: - """检查设备是否可用""" - try: - self.get_device_status() - return True - except: - return False - - # ============ 物料系统接口 ============ - - def get_deck(self) -> Deck: - """获取主 Deck""" - return self.deck - - def get_all_resources(self) -> Dict[str, PLRResource]: - """获取所有 PLR 资源""" - return self.plr_resources.copy() - - def find_resource_by_name(self, name: str) -> Optional[PLRResource]: - """按名称查找资源""" - return self.plr_resources.get(name) - - def find_resources_by_type(self, resource_type: type) -> List[PLRResource]: - """按类型查找资源""" - return [res for res in self.plr_resources.values() if isinstance(res, resource_type)] - - async def sync_with_external_system(self) -> bool: - """与外部物料系统同步""" - if not self.resource_synchronizer: - logger.info(f"工作站 {self.device_id} 没有配置资源同步器") - return True - - try: - success = await self.resource_synchronizer.sync_from_external() - if success: - logger.info(f"工作站 {self.device_id} 外部同步成功") - else: - logger.warning(f"工作站 {self.device_id} 外部同步失败") - return success - except Exception as e: - logger.error(f"工作站 {self.device_id} 外部同步异常: {e}") - return False - - # ============ 简化的工作流控制 ============ - - def execute_workflow(self, workflow_name: str, parameters: Dict[str, Any]) -> bool: - """执行工作流""" - try: - # 设置工作流状态 - self.current_workflow_status = WorkflowStatus.INITIALIZING - self.workflow_parameters = parameters - self.workflow_start_time = time.time() - - # 委托给子类实现 - success = self._execute_workflow_impl(workflow_name, parameters) - - if success: - self.current_workflow_status = WorkflowStatus.RUNNING - logger.info(f"工作站 {self.device_id} 工作流 {workflow_name} 启动成功") - else: - self.current_workflow_status = WorkflowStatus.ERROR - logger.error(f"工作站 {self.device_id} 工作流 {workflow_name} 启动失败") - - return success - - except Exception as e: - self.current_workflow_status = WorkflowStatus.ERROR - logger.error(f"工作站 {self.device_id} 执行工作流失败: {e}") - return False - - def stop_workflow(self, emergency: bool = False) -> bool: - """停止工作流""" - try: - if self.current_workflow_status in [WorkflowStatus.IDLE, WorkflowStatus.STOPPED]: - logger.warning(f"工作站 {self.device_id} 没有正在运行的工作流") - return True - - self.current_workflow_status = WorkflowStatus.STOPPING - - # 委托给子类实现 - success = self._stop_workflow_impl(emergency) - - if success: - self.current_workflow_status = WorkflowStatus.STOPPED - logger.info(f"工作站 {self.device_id} 工作流停止成功 (紧急: {emergency})") - else: - self.current_workflow_status = WorkflowStatus.ERROR - logger.error(f"工作站 {self.device_id} 工作流停止失败") - - return success - - except Exception as e: - self.current_workflow_status = WorkflowStatus.ERROR - logger.error(f"工作站 {self.device_id} 停止工作流失败: {e}") - return False - - # ============ 状态属性 ============ - - @property - def workflow_status(self) -> WorkflowStatus: - """获取当前工作流状态""" - return self.current_workflow_status - - @property - def is_busy(self) -> bool: - """检查工作站是否忙碌""" - return self.current_workflow_status in [ - WorkflowStatus.INITIALIZING, - WorkflowStatus.RUNNING, - WorkflowStatus.STOPPING, - ] - - @property - def workflow_runtime(self) -> float: - """获取工作流运行时间(秒)""" - if self.workflow_start_time is None: - return 0.0 - return time.time() - self.workflow_start_time - - # ============ 抽象方法 - 子类必须实现 ============ - - # @abstractmethod - # def _register_supported_workflows(self): - # """注册支持的工作流 - 子类必须实现""" - # pass - - # @abstractmethod - # def _execute_workflow_impl(self, workflow_name: str, parameters: Dict[str, Any]) -> bool: - # """执行工作流的具体实现 - 子类必须实现""" - # pass - - # @abstractmethod - # def _stop_workflow_impl(self, emergency: bool = False) -> bool: - # """停止工作流的具体实现 - 子类必须实现""" - # pass - -class WorkstationExample(WorkstationBase): - """工作站示例实现""" - - def _register_supported_workflows(self): - """注册支持的工作流""" - self.supported_workflows["example_workflow"] = WorkflowInfo( - name="example_workflow", - description="这是一个示例工作流", - estimated_duration=300.0, - required_materials=["sample_plate"], - output_product="processed_plate", - parameters_schema={"param1": "string", "param2": "integer"}, - ) - - def _execute_workflow_impl(self, workflow_name: str, parameters: Dict[str, Any]) -> bool: - """执行工作流的具体实现""" - if workflow_name not in self.supported_workflows: - logger.error(f"工作站 {self.device_id} 不支持工作流: {workflow_name}") - return False - - # 这里添加实际的工作流逻辑 - logger.info(f"工作站 {self.device_id} 正在执行工作流: {workflow_name} with parameters {parameters}") - return True - - def _stop_workflow_impl(self, emergency: bool = False) -> bool: - """停止工作流的具体实现""" - # 这里添加实际的停止逻辑 - logger.info(f"工作站 {self.device_id} 正在停止工作流 (紧急: {emergency})") - return True \ No newline at end of file diff --git a/unilabos/registry/devices/bioyond.yaml b/unilabos/registry/devices/bioyond.yaml deleted file mode 100644 index fe6f0907..00000000 --- a/unilabos/registry/devices/bioyond.yaml +++ /dev/null @@ -1,255 +0,0 @@ -workstation.bioyond_dispensing_station: - category: - - workstation - - bioyond - class: - action_value_mappings: - create_90_10_vial_feeding_task: - feedback: {} - goal: - delay_time: delay_time - hold_m_name: hold_m_name - order_name: order_name - percent_10_1_assign_material_name: percent_10_1_assign_material_name - percent_10_1_liquid_material_name: percent_10_1_liquid_material_name - percent_10_1_target_weigh: percent_10_1_target_weigh - percent_10_1_volume: percent_10_1_volume - percent_10_2_assign_material_name: percent_10_2_assign_material_name - percent_10_2_liquid_material_name: percent_10_2_liquid_material_name - percent_10_2_target_weigh: percent_10_2_target_weigh - percent_10_2_volume: percent_10_2_volume - percent_10_3_assign_material_name: percent_10_3_assign_material_name - percent_10_3_liquid_material_name: percent_10_3_liquid_material_name - percent_10_3_target_weigh: percent_10_3_target_weigh - percent_10_3_volume: percent_10_3_volume - percent_90_1_assign_material_name: percent_90_1_assign_material_name - percent_90_1_target_weigh: percent_90_1_target_weigh - percent_90_2_assign_material_name: percent_90_2_assign_material_name - percent_90_2_target_weigh: percent_90_2_target_weigh - percent_90_3_assign_material_name: percent_90_3_assign_material_name - percent_90_3_target_weigh: percent_90_3_target_weigh - speed: speed - temperature: temperature - goal_default: - delay_time: '' - hold_m_name: '' - order_name: '' - percent_10_1_assign_material_name: '' - percent_10_1_liquid_material_name: '' - percent_10_1_target_weigh: '' - percent_10_1_volume: '' - percent_10_2_assign_material_name: '' - percent_10_2_liquid_material_name: '' - percent_10_2_target_weigh: '' - percent_10_2_volume: '' - percent_10_3_assign_material_name: '' - percent_10_3_liquid_material_name: '' - percent_10_3_target_weigh: '' - percent_10_3_volume: '' - percent_90_1_assign_material_name: '' - percent_90_1_target_weigh: '' - percent_90_2_assign_material_name: '' - percent_90_2_target_weigh: '' - percent_90_3_assign_material_name: '' - percent_90_3_target_weigh: '' - speed: '' - temperature: '' - handles: {} - result: - return_info: return_info - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: DispenStationVialFeed_Feedback - type: object - goal: - properties: - delay_time: - type: string - hold_m_name: - type: string - order_name: - type: string - percent_10_1_assign_material_name: - type: string - percent_10_1_liquid_material_name: - type: string - percent_10_1_target_weigh: - type: string - percent_10_1_volume: - type: string - percent_10_2_assign_material_name: - type: string - percent_10_2_liquid_material_name: - type: string - percent_10_2_target_weigh: - type: string - percent_10_2_volume: - type: string - percent_10_3_assign_material_name: - type: string - percent_10_3_liquid_material_name: - type: string - percent_10_3_target_weigh: - type: string - percent_10_3_volume: - type: string - percent_90_1_assign_material_name: - type: string - percent_90_1_target_weigh: - type: string - percent_90_2_assign_material_name: - type: string - percent_90_2_target_weigh: - type: string - percent_90_3_assign_material_name: - type: string - percent_90_3_target_weigh: - type: string - speed: - type: string - temperature: - type: string - required: - - order_name - - percent_90_1_assign_material_name - - percent_90_1_target_weigh - - percent_90_2_assign_material_name - - percent_90_2_target_weigh - - percent_90_3_assign_material_name - - percent_90_3_target_weigh - - percent_10_1_assign_material_name - - percent_10_1_target_weigh - - percent_10_1_volume - - percent_10_1_liquid_material_name - - percent_10_2_assign_material_name - - percent_10_2_target_weigh - - percent_10_2_volume - - percent_10_2_liquid_material_name - - percent_10_3_assign_material_name - - percent_10_3_target_weigh - - percent_10_3_volume - - percent_10_3_liquid_material_name - - speed - - temperature - - delay_time - - hold_m_name - title: DispenStationVialFeed_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: DispenStationVialFeed_Result - type: object - required: - - goal - title: DispenStationVialFeed - type: object - type: DispenStationVialFeed - create_diamine_solution_task: - feedback: {} - goal: - delay_time: delay_time - hold_m_name: hold_m_name - liquid_material_name: liquid_material_name - material_name: material_name - order_name: order_name - speed: speed - target_weigh: target_weigh - temperature: temperature - volume: volume - goal_default: - delay_time: '' - hold_m_name: '' - liquid_material_name: '' - material_name: '' - order_name: '' - speed: '' - target_weigh: '' - temperature: '' - volume: '' - handles: {} - result: - return_info: return_info - schema: - description: '' - properties: - feedback: - properties: {} - required: [] - title: DispenStationSolnPrep_Feedback - type: object - goal: - properties: - delay_time: - type: string - hold_m_name: - type: string - liquid_material_name: - type: string - material_name: - type: string - order_name: - type: string - speed: - type: string - target_weigh: - type: string - temperature: - type: string - volume: - type: string - required: - - order_name - - material_name - - target_weigh - - volume - - liquid_material_name - - speed - - temperature - - delay_time - - hold_m_name - title: DispenStationSolnPrep_Goal - type: object - result: - properties: - return_info: - type: string - required: - - return_info - title: DispenStationSolnPrep_Result - type: object - required: - - goal - title: DispenStationSolnPrep - type: object - type: DispenStationSolnPrep - module: unilabos.devices.workstation.bioyond_studio.dispensing_station:BioyondDispensingStation - status_types: {} - type: python - config_info: [] - description: '' - handles: [] - icon: '' - init_param_schema: - config: - properties: - config: - type: string - deck: - type: string - required: - - config - - deck - type: object - data: - properties: {} - required: [] - type: object - version: 1.0.0 diff --git a/unilabos/registry/devices/laiyu_liquid.yaml b/unilabos/registry/devices/laiyu_liquid.yaml index 64c0c182..98201a7d 100644 --- a/unilabos/registry/devices/laiyu_liquid.yaml +++ b/unilabos/registry/devices/laiyu_liquid.yaml @@ -1361,7 +1361,8 @@ laiyu_liquid: mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' - mix_times: 0 + mix_times: + - 0 mix_vol: 0 none_keys: - '' @@ -1491,9 +1492,11 @@ laiyu_liquid: mix_stage: type: string mix_times: - maximum: 2147483647 - minimum: -2147483648 - type: integer + items: + maximum: 2147483647 + minimum: -2147483648 + type: integer + type: array mix_vol: maximum: 2147483647 minimum: -2147483648 diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml index 99c92333..b21ccd7e 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -3994,7 +3994,8 @@ liquid_handler: mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' - mix_times: 0 + mix_times: + - 0 mix_vol: 0 none_keys: - '' @@ -4150,9 +4151,11 @@ liquid_handler: mix_stage: type: string mix_times: - maximum: 2147483647 - minimum: -2147483648 - type: integer + items: + maximum: 2147483647 + minimum: -2147483648 + type: integer + type: array mix_vol: maximum: 2147483647 minimum: -2147483648 @@ -5012,7 +5015,8 @@ liquid_handler.biomek: mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' - mix_times: 0 + mix_times: + - 0 mix_vol: 0 none_keys: - '' @@ -5155,9 +5159,11 @@ liquid_handler.biomek: mix_stage: type: string mix_times: - maximum: 2147483647 - minimum: -2147483648 - type: integer + items: + maximum: 2147483647 + minimum: -2147483648 + type: integer + type: array mix_vol: maximum: 2147483647 minimum: -2147483648 @@ -7801,7 +7807,8 @@ liquid_handler.prcxi: mix_liquid_height: 0.0 mix_rate: 0 mix_stage: '' - mix_times: 0 + mix_times: + - 0 mix_vol: 0 none_keys: - '' @@ -7930,9 +7937,11 @@ liquid_handler.prcxi: mix_stage: type: string mix_times: - maximum: 2147483647 - minimum: -2147483648 - type: integer + items: + maximum: 2147483647 + minimum: -2147483648 + type: integer + type: array mix_vol: maximum: 2147483647 minimum: -2147483648 diff --git a/unilabos/registry/devices/reaction_station_bioyond.yaml b/unilabos/registry/devices/reaction_station_bioyond.yaml index 875de078..f1a16ec2 100644 --- a/unilabos/registry/devices/reaction_station_bioyond.yaml +++ b/unilabos/registry/devices/reaction_station_bioyond.yaml @@ -4,77 +4,6 @@ reaction_station.bioyond: - reaction_station_bioyond class: action_value_mappings: - auto-append_to_workflow_sequence: - feedback: {} - goal: {} - goal_default: - web_workflow_name: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - web_workflow_name: - type: string - required: - - web_workflow_name - type: object - result: {} - required: - - goal - title: append_to_workflow_sequence参数 - type: object - type: UniLabJsonCommand - auto-clear_workflows: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: clear_workflows参数 - type: object - type: UniLabJsonCommand - auto-load_bioyond_data_from_file: - feedback: {} - goal: {} - goal_default: - file_path: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - file_path: - type: string - required: - - file_path - type: object - result: {} - required: - - goal - title: load_bioyond_data_from_file参数 - type: object - type: UniLabJsonCommand auto-post_init: feedback: {} goal: {} @@ -116,397 +45,35 @@ reaction_station.bioyond: properties: json_str: type: string - required: - - json_str - type: object - result: {} - required: - - goal - title: process_web_workflows参数 - type: object - type: UniLabJsonCommand - auto-reset_workstation: - feedback: {} - goal: {} - goal_default: {} - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: {} - required: [] - type: object - result: {} - required: - - goal - title: reset_workstation参数 - type: object - type: UniLabJsonCommand - auto-resource_tree_add: - feedback: {} - goal: {} - goal_default: - resources: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - resources: - items: - type: object - type: array - required: - - resources - type: object - result: {} - required: - - goal - title: resource_tree_add参数 - type: object - type: UniLabJsonCommand - auto-set_workflow_sequence: - feedback: {} - goal: {} - goal_default: - json_str: null - handles: {} - placeholder_keys: {} - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - json_str: - type: string - required: - - json_str - type: object - result: {} - required: - - goal - title: set_workflow_sequence参数 - type: object - type: UniLabJsonCommand - auto-transfer_resource_to_another: - feedback: {} - goal: {} - goal_default: - mount_device_id: null - mount_resource: null - resource: null - sites: null - handles: {} - placeholder_keys: - mount_device_id: unilabos_devices - mount_resource: unilabos_resources - resource: unilabos_resources - result: {} - schema: - description: '' - properties: - feedback: {} - goal: - properties: - mount_device_id: - type: object - mount_resource: - items: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: mount_resource - type: object - type: array - resource: - items: - properties: - category: - type: string - children: - items: - type: string - type: array - config: - type: string - data: - type: string - id: - type: string - name: - type: string - parent: - type: string - pose: - properties: - orientation: - properties: - w: - type: number - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - - w - title: orientation - type: object - position: - properties: - x: - type: number - y: - type: number - z: - type: number - required: - - x - - y - - z - title: position - type: object - required: - - position - - orientation - title: pose - type: object - sample_id: - type: string - type: - type: string - required: - - id - - name - - sample_id - - children - - parent - - type - - category - - pose - - config - - data - title: resource - type: object - type: array - sites: - items: - type: string - type: array - required: - - resource - - mount_resource - - sites - - mount_device_id - type: object - result: {} - required: - - goal - title: transfer_resource_to_another参数 - type: object - type: UniLabJsonCommand - bioyond_sync: - feedback: {} - goal: - force_sync: force_sync - sync_type: sync_type - goal_default: - force_sync: false - sync_type: full - handles: {} - result: {} - schema: - description: 从Bioyond系统同步物料 - properties: - feedback: {} - goal: - properties: - force_sync: - description: 是否强制同步 - type: boolean - sync_type: - description: 同步类型 - enum: - - full - - incremental - type: string - required: - - sync_type - type: object - result: {} - required: - - goal - title: bioyond_sync参数 - type: object - type: UniLabJsonCommand - bioyond_update: - feedback: {} - goal: - material_ids: material_ids - sync_all: sync_all - goal_default: - material_ids: [] - sync_all: true - handles: {} - result: {} - schema: - description: 将本地物料变更同步到Bioyond - properties: - feedback: {} - goal: - properties: - material_ids: - description: 要同步的物料ID列表 - items: - type: string - type: array - sync_all: - description: 是否同步所有物料 - type: boolean - required: - - sync_all - type: object - result: {} - required: - - goal - title: bioyond_update参数 - type: object - type: UniLabJsonCommand - reaction_station_drip_back: - feedback: {} - goal: - assign_material_name: assign_material_name - time: time - torque_variation: torque_variation - volume: volume - goal_default: - assign_material_name: '' - time: '' - torque_variation: '' - volume: '' - handles: {} - result: {} - schema: - description: 反应站滴回操作 - properties: - feedback: {} - goal: - properties: - assign_material_name: - description: 溶剂名称 - type: string - time: - description: 观察时间(单位min) - type: string - torque_variation: - description: 是否观察1否2是 - type: string volume: - description: 投料体积 + description: 分液公式(μL) type: string required: - volume - assign_material_name - time - torque_variation + - titration_type + - temperature type: object result: {} required: - goal - title: reaction_station_drip_back参数 + title: drip_back参数 type: object type: UniLabJsonCommand - reaction_station_liquid_feed: + drip_back: feedback: {} goal: assign_material_name: assign_material_name + temperature: temperature time: time titration_type: titration_type torque_variation: torque_variation volume: volume goal_default: assign_material_name: '' + temperature: '' time: '' titration_type: '' torque_variation: '' @@ -514,40 +81,265 @@ reaction_station.bioyond: handles: {} result: {} schema: - description: 反应站液体进料操作 + description: 滴回去 properties: feedback: {} goal: properties: assign_material_name: - description: 溶剂名称 + description: 物料名称(不能为空) + type: string + temperature: + description: 温度设定(°C) type: string time: - description: 观察时间(单位min) - type: string - titration_type: - description: 滴定类型1否2是 - type: string - torque_variation: - description: 是否观察1否2是 - type: string - volume: - description: 投料体积 + description: 观察时间(分钟) type: string required: - - titration_type - - volume - - assign_material_name - - time - - torque_variation + - file_path type: object result: {} required: - goal - title: reaction_station_liquid_feed参数 + title: load_bioyond_data_from_file参数 type: object type: UniLabJsonCommand - reaction_station_process_execute: + liquid_feeding_beaker: + feedback: {} + goal: + assign_material_name: assign_material_name + temperature: temperature + time: time + titration_type: titration_type + torque_variation: torque_variation + volume: volume + goal_default: + assign_material_name: '' + temperature: '' + time: '' + titration_type: '' + torque_variation: '' + volume: '' + handles: {} + result: {} + schema: + description: 液体进料烧杯 + properties: + feedback: {} + goal: + properties: + assign_material_name: + description: 物料名称 + type: string + temperature: + description: 温度设定(°C) + type: string + time: + description: 观察时间(分钟) + type: string + titration_type: + description: 是否滴定(1=否, 2=是) + type: string + torque_variation: + description: 是否观察 (1=否, 2=是) + type: string + volume: + description: 分液公式(μL) + type: string + required: + - volume + - assign_material_name + - time + - torque_variation + - titration_type + - temperature + type: object + result: {} + required: + - goal + title: liquid_feeding_beaker参数 + type: object + type: UniLabJsonCommand + liquid_feeding_solvents: + feedback: {} + goal: + assign_material_name: assign_material_name + solvents: solvents + temperature: temperature + time: time + titration_type: titration_type + torque_variation: torque_variation + volume: volume + goal_default: + assign_material_name: '' + solvents: '' + temperature: '25.00' + time: '360' + titration_type: '1' + torque_variation: '2' + volume: '' + handles: + input: + - data_key: solvents + data_source: handle + data_type: object + handler_key: solvents + io_type: source + label: Solvents Data From Calculation Node + result: {} + schema: + description: 液体投料-溶剂。可以直接提供volume(μL),或通过solvents对象自动从additional_solvent(mL)计算volume。 + properties: + feedback: {} + goal: + properties: + assign_material_name: + description: 物料名称 + type: string + solvents: + description: '溶剂信息对象(可选),包含: additional_solvent(溶剂体积mL), total_liquid_volume(总液体体积mL)。如果提供,将自动计算volume' + type: string + temperature: + default: '25.00' + description: 温度设定(°C),默认25.00 + type: string + time: + default: '360' + description: 观察时间(分钟),默认360 + type: string + titration_type: + default: '1' + description: 是否滴定(1=否, 2=是),默认1 + type: string + torque_variation: + default: '2' + description: 是否观察 (1=否, 2=是),默认2 + type: string + volume: + description: 分液量(μL)。可直接提供,或通过solvents参数自动计算 + type: string + required: + - assign_material_name + type: object + result: {} + required: + - goal + title: liquid_feeding_solvents参数 + type: object + type: UniLabJsonCommand + liquid_feeding_titration: + feedback: {} + goal: + assign_material_name: assign_material_name + temperature: temperature + time: time + titration_type: titration_type + torque_variation: torque_variation + volume_formula: volume_formula + goal_default: + assign_material_name: '' + temperature: '' + time: '' + titration_type: '' + torque_variation: '' + volume_formula: '' + handles: {} + result: {} + schema: + description: 液体进料(滴定) + properties: + feedback: {} + goal: + properties: + assign_material_name: + description: 物料名称 + type: string + temperature: + description: 温度设定(°C) + type: string + time: + description: 观察时间(分钟) + type: string + titration_type: + description: 是否滴定(1=否, 2=是) + type: string + torque_variation: + description: 是否观察 (1=否, 2=是) + type: string + volume_formula: + description: 分液公式(μL) + type: string + required: + - volume_formula + - assign_material_name + - time + - torque_variation + - titration_type + - temperature + type: object + result: {} + required: + - goal + title: liquid_feeding_titration参数 + type: object + type: UniLabJsonCommand + liquid_feeding_vials_non_titration: + feedback: {} + goal: + assign_material_name: assign_material_name + temperature: temperature + time: time + titration_type: titration_type + torque_variation: torque_variation + volume_formula: volume_formula + goal_default: + assign_material_name: '' + temperature: '' + time: '' + titration_type: '' + torque_variation: '' + volume_formula: '' + handles: {} + result: {} + schema: + description: 液体进料小瓶(非滴定) + properties: + feedback: {} + goal: + properties: + assign_material_name: + description: 物料名称 + type: string + temperature: + description: 温度设定(°C) + type: string + time: + description: 观察时间(分钟) + type: string + titration_type: + description: 是否滴定(1=否, 2=是) + type: string + torque_variation: + description: 是否观察 (1=否, 2=是) + type: string + volume_formula: + description: 分液公式(μL) + type: string + required: + - volume_formula + - assign_material_name + - time + - torque_variation + - titration_type + - temperature + type: object + result: {} + required: + - goal + title: liquid_feeding_vials_non_titration参数 + type: object + type: UniLabJsonCommand + process_and_execute_workflow: feedback: {} goal: task_name: task_name @@ -558,7 +350,7 @@ reaction_station.bioyond: handles: {} result: {} schema: - description: 反应站流程执行 + description: 处理并执行工作流 properties: feedback: {} goal: @@ -576,92 +368,10 @@ reaction_station.bioyond: result: {} required: - goal - title: reaction_station_process_execute参数 + title: process_and_execute_workflow参数 type: object type: UniLabJsonCommand - reaction_station_reactor_taken_out: - feedback: {} - goal: - order_id: order_id - preintake_id: preintake_id - goal_default: - order_id: '' - preintake_id: '' - handles: {} - result: {} - schema: - description: 反应站反应器取出操作 - 通过订单ID和预取样ID进行精确控制 - properties: - feedback: {} - goal: - properties: - order_id: - description: 订单ID,用于标识要取出的订单 - type: string - preintake_id: - description: 预取样ID,用于标识具体的取样任务 - type: string - required: [] - type: object - result: - properties: - code: - description: 操作结果代码(1表示成功,0表示失败) - type: integer - return_info: - description: 操作结果详细信息 - type: string - type: object - required: - - goal - title: reaction_station_reactor_taken_out参数 - type: object - type: UniLabJsonCommand - reaction_station_solid_feed_vial: - feedback: {} - goal: - assign_material_name: assign_material_name - material_id: material_id - time: time - torque_variation: torque_variation - goal_default: - assign_material_name: '' - material_id: '' - time: '' - torque_variation: '' - handles: {} - result: {} - schema: - description: 反应站固体进料操作 - properties: - feedback: {} - goal: - properties: - assign_material_name: - description: 固体名称_粉末加样模块-投料 - type: string - material_id: - description: 固体投料类型_粉末加样模块-投料 - type: string - time: - description: 观察时间_反应模块-观察搅拌结果 - type: string - torque_variation: - description: 是否观察1否2是_反应模块-观察搅拌结果 - type: string - required: - - assign_material_name - - material_id - - time - - torque_variation - type: object - result: {} - required: - - goal - title: reaction_station_solid_feed_vial参数 - type: object - type: UniLabJsonCommand - reaction_station_take_in: + reactor_taken_in: feedback: {} goal: assign_material_name: assign_material_name @@ -674,7 +384,7 @@ reaction_station.bioyond: handles: {} result: {} schema: - description: 反应站取入操作 + description: 反应器放入 - 将反应器放入工作站,配置物料名称、粘度上限和温度参数 properties: feedback: {} goal: @@ -683,10 +393,10 @@ reaction_station.bioyond: description: 物料名称 type: string cutoff: - description: 截止参数 + description: 粘度上限 type: string temperature: - description: 温度 + description: 温度设定(°C) type: string required: - cutoff @@ -696,10 +406,88 @@ reaction_station.bioyond: result: {} required: - goal - title: reaction_station_take_in参数 + title: reactor_taken_in参数 type: object type: UniLabJsonCommand - module: unilabos.devices.workstation.bioyond_studio.station:BioyondWorkstation + reactor_taken_out: + feedback: {} + goal: {} + goal_default: {} + handles: {} + result: {} + schema: + description: 反应器取出 - 从工作站中取出反应器,无需参数的简单操作 + properties: + feedback: {} + goal: + properties: {} + required: [] + type: object + result: + properties: + code: + description: 操作结果代码(1表示成功,0表示失败) + type: integer + return_info: + description: 操作结果详细信息 + type: string + type: object + required: + - goal + title: reactor_taken_out参数 + type: object + type: UniLabJsonCommand + solid_feeding_vials: + feedback: {} + goal: + assign_material_name: assign_material_name + material_id: material_id + temperature: temperature + time: time + torque_variation: torque_variation + goal_default: + assign_material_name: '' + material_id: '' + temperature: '' + time: '' + torque_variation: '' + handles: {} + result: {} + schema: + description: 固体进料小瓶 - 通过小瓶向反应器中添加固体物料,支持多种粉末类型(盐、面粉、BTDA) + properties: + feedback: {} + goal: + properties: + assign_material_name: + description: 物料名称(用于获取试剂瓶位ID) + type: string + material_id: + description: 粉末类型ID,1=盐(21分钟),2=面粉(27分钟),3=BTDA(38分钟) + type: string + temperature: + description: 温度设定(°C) + type: string + time: + description: 观察时间(分钟) + type: string + torque_variation: + description: 是否观察 (1=否, 2=是) + type: string + required: + - assign_material_name + - material_id + - time + - torque_variation + - temperature + type: object + result: {} + required: + - goal + title: solid_feeding_vials参数 + type: object + type: UniLabJsonCommand + module: unilabos.devices.workstation.bioyond_studio.reaction_station:BioyondReactionStation protocol_type: [] status_types: all_workflows: dict @@ -708,14 +496,14 @@ reaction_station.bioyond: workstation_status: dict type: python config_info: [] - description: Bioyond反应站 - 专门用于化学反应操作的工作站 + description: Bioyond反应站 handles: [] - icon: 反应站.webp + icon: reaction_station.webp init_param_schema: config: properties: - bioyond_config: - type: string + config: + type: object deck: type: string required: [] diff --git a/unilabos/resources/bioyond/warehouses.py b/unilabos/resources/bioyond/warehouses.py index c546759d..6eb4f26e 100644 --- a/unilabos/resources/bioyond/warehouses.py +++ b/unilabos/resources/bioyond/warehouses.py @@ -18,6 +18,7 @@ def bioyond_warehouse_1x4x4(name: str) -> WareHouse: ) + def bioyond_warehouse_1x4x2(name: str) -> WareHouse: """创建BioYond 4x1x2仓库""" return warehouse_factory( diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index 92fcf1ed..a6c2f30b 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -535,6 +535,7 @@ def resource_ulab_to_plr(resource: dict, plr_model=False) -> "ResourcePLR": def resource_ulab_to_plr_inner(resource: dict): all_states[resource["name"]] = resource["data"] + extra = resource.pop("extra", {}) d = { "name": resource["name"], "type": resource["type"], @@ -575,16 +576,16 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None, w replace_info = { "plate": "plate", "well": "well", - "tip_spot": "container", - "trash": "container", + "tip_spot": "tip_spot", + "trash": "trash", "deck": "deck", - "tip_rack": "container", + "tip_rack": "tip_rack", } if source in replace_info: return replace_info[source] else: print("转换pylabrobot的时候,出现未知类型", source) - return "container" + return source def resource_plr_to_ulab_inner(d: dict, all_states: dict, child=True) -> dict: r = { diff --git a/unilabos/resources/itemized_carrier.py b/unilabos/resources/itemized_carrier.py index 7607cf4d..fef09e25 100644 --- a/unilabos/resources/itemized_carrier.py +++ b/unilabos/resources/itemized_carrier.py @@ -78,6 +78,7 @@ class ItemizedCarrier(ResourcePLR): sites: Optional[Dict[Union[int, str], Optional[ResourcePLR]]] = None, category: Optional[str] = "carrier", model: Optional[str] = None, + invisible_slots: Optional[str] = None, ): super().__init__( name=name, @@ -89,6 +90,7 @@ class ItemizedCarrier(ResourcePLR): ) self.num_items = len(sites) self.num_items_x, self.num_items_y, self.num_items_z = num_items_x, num_items_y, num_items_z + self.invisible_slots = [] if invisible_slots is None else invisible_slots self.layout = "z-y" if self.num_items_z > 1 and self.num_items_x == 1 else "x-z" if self.num_items_z > 1 and self.num_items_y == 1 else "x-y" if isinstance(sites, dict): @@ -410,7 +412,7 @@ class ItemizedCarrier(ResourcePLR): "layout": self.layout, "sites": [{ "label": str(identifier), - "visible": True if self[identifier] is not None else False, + "visible": False if identifier in self.invisible_slots else True, "occupied_by": self[identifier].name if isinstance(self[identifier], ResourcePLR) and not isinstance(self[identifier], ResourceHolder) else self[identifier] if isinstance(self[identifier], str) else None, @@ -433,6 +435,7 @@ class BottleCarrier(ItemizedCarrier): sites: Optional[Dict[Union[int, str], ResourceHolder]] = None, category: str = "bottle_carrier", model: Optional[str] = None, + invisible_slots: List[str] = None, **kwargs, ): super().__init__( @@ -443,4 +446,5 @@ class BottleCarrier(ItemizedCarrier): sites=sites, category=category, model=model, + invisible_slots=invisible_slots, ) diff --git a/unilabos/ros/main_slave_run.py b/unilabos/ros/main_slave_run.py index b57d30b0..d9ad3682 100644 --- a/unilabos/ros/main_slave_run.py +++ b/unilabos/ros/main_slave_run.py @@ -10,7 +10,7 @@ from unilabos.ros.nodes.presets.resource_mesh_manager import ResourceMeshManager from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker, ResourceTreeSet from unilabos.devices.ros_dev.liquid_handler_joint_publisher import LiquidHandlerJointPublisher from unilabos_msgs.srv import SerialCommand # type: ignore -from rclpy.executors import MultiThreadedExecutor +from rclpy.executors import MultiThreadedExecutor, SingleThreadedExecutor from rclpy.node import Node from rclpy.timer import Timer diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index 289fe513..f1063123 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -49,7 +49,7 @@ from unilabos_msgs.msg import Resource # type: ignore from unilabos.ros.nodes.resource_tracker import ( DeviceNodeResourceTracker, - ResourceTreeSet, + ResourceTreeSet, ResourceTreeInstance, ) from unilabos.ros.x.rclpyx import get_event_loop from unilabos.ros.utils.driver_creator import WorkstationNodeCreator, PyLabRobotCreator, DeviceClassCreator @@ -338,12 +338,12 @@ class BaseROS2DeviceNode(Node, Generic[T]): # 创建资源管理客户端 self._resource_clients: Dict[str, Client] = { - "resource_add": self.create_client(ResourceAdd, "/resources/add"), - "resource_get": self.create_client(SerialCommand, "/resources/get"), - "resource_delete": self.create_client(ResourceDelete, "/resources/delete"), - "resource_update": self.create_client(ResourceUpdate, "/resources/update"), - "resource_list": self.create_client(ResourceList, "/resources/list"), - "c2s_update_resource_tree": self.create_client(SerialCommand, "/c2s_update_resource_tree"), + "resource_add": self.create_client(ResourceAdd, "/resources/add", callback_group=self.callback_group), + "resource_get": self.create_client(SerialCommand, "/resources/get", callback_group=self.callback_group), + "resource_delete": self.create_client(ResourceDelete, "/resources/delete", callback_group=self.callback_group), + "resource_update": self.create_client(ResourceUpdate, "/resources/update", callback_group=self.callback_group), + "resource_list": self.create_client(ResourceList, "/resources/list", callback_group=self.callback_group), + "c2s_update_resource_tree": self.create_client(SerialCommand, "/c2s_update_resource_tree", callback_group=self.callback_group), } def re_register_device(req, res): @@ -573,6 +573,52 @@ class BaseROS2DeviceNode(Node, Generic[T]): self.lab_logger().error(traceback.format_exc()) self.lab_logger().debug(f"资源更新结果: {response}") + def transfer_to_new_resource(self, plr_resource: "ResourcePLR", tree: ResourceTreeInstance, additional_add_params: Dict[str, Any]): + parent_uuid = tree.root_node.res_content.parent_uuid + if parent_uuid: + parent_resource: ResourcePLR = self.resource_tracker.uuid_to_resources.get(parent_uuid) + if parent_resource is None: + self.lab_logger().warning( + f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_uuid}不存在" + ) + else: + try: + # 特殊兼容所有plr的物料的assign方法,和create_resource append_resource后期同步 + additional_params = {} + extra = getattr(plr_resource, "unilabos_extra", {}) + if len(extra): + self.lab_logger().info(f"发现物料{plr_resource}额外参数: " + str(extra)) + if "update_resource_site" in extra: + additional_add_params["site"] = extra["update_resource_site"] + site = additional_add_params.get("site", None) + spec = inspect.signature(parent_resource.assign_child_resource) + if "spot" in spec.parameters: + ordering_dict: Dict[str, Any] = getattr(parent_resource, "_ordering") + if ordering_dict: + site = list(ordering_dict.keys()).index(site) + additional_params["spot"] = site + old_parent = plr_resource.parent + if old_parent is not None: + # plr并不支持同一个deck的加载和卸载 + self.lab_logger().warning( + f"物料{plr_resource}请求从{old_parent}卸载" + ) + old_parent.unassign_child_resource(plr_resource) + self.lab_logger().warning( + f"物料{plr_resource}请求挂载到{parent_resource},额外参数:{additional_params}" + ) + parent_resource.assign_child_resource( + plr_resource, location=None, **additional_params + ) + func = getattr(self.driver_instance, "resource_tree_transfer", None) + if callable(func): + # 分别是 物料的原来父节点,当前物料的状态,物料的新父节点(此时物料已经重新assign了) + func(old_parent, plr_resource, parent_resource) + except Exception as e: + self.lab_logger().warning( + f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_resource}[{parent_uuid}]失败!\n{traceback.format_exc()}" + ) + async def s2c_resource_tree(self, req: SerialCommand_Request, res: SerialCommand_Response): """ 处理资源树更新请求 @@ -613,28 +659,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): plr_resources = tree_set.to_plr_resources() for plr_resource, tree in zip(plr_resources, tree_set.trees): self.resource_tracker.add_resource(plr_resource) - parent_uuid = tree.root_node.res_content.parent_uuid - if parent_uuid: - parent_resource: ResourcePLR = self.resource_tracker.uuid_to_resources.get(parent_uuid) - if parent_resource is None: - self.lab_logger().warning( - f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_uuid}不存在" - ) - else: - try: - # 特殊兼容所有plr的物料的assign方法,和create_resource append_resource后期同步 - additional_params = {} - site = additional_add_params.get("site", None) - spec = inspect.signature(parent_resource.assign_child_resource) - if "spot" in spec.parameters: - additional_params["spot"] = site - parent_resource.assign_child_resource( - plr_resource, location=None, **additional_params - ) - except Exception as e: - self.lab_logger().warning( - f"物料{plr_resource}请求挂载{tree.root_node.res_content.name}的父节点{parent_resource}[{parent_uuid}]失败!\n{traceback.format_exc()}" - ) + self.transfer_to_new_resource(plr_resource, tree, additional_add_params) func = getattr(self.driver_instance, "resource_tree_add", None) if callable(func): func(plr_resources) @@ -647,6 +672,17 @@ class BaseROS2DeviceNode(Node, Generic[T]): original_instance: ResourcePLR = self.resource_tracker.figure_resource( {"uuid": tree.root_node.res_content.uuid}, try_mode=False ) + original_parent_resource = original_instance.parent + original_parent_resource_uuid = getattr(original_parent_resource, "unilabos_uuid", None) + target_parent_resource_uuid = tree.root_node.res_content.uuid_parent + self.lab_logger().info( + f"物料{original_instance} 原始父节点{original_parent_resource_uuid} 目标父节点{target_parent_resource_uuid} 更新" + ) + # todo: 对extra进行update + if getattr(plr_resource, "unilabos_extra", None) is not None: + original_instance.unilabos_extra = getattr(plr_resource, "unilabos_extra") + if original_parent_resource_uuid != target_parent_resource_uuid and original_parent_resource is not None: + self.transfer_to_new_resource(original_instance, tree, additional_add_params) original_instance.load_all_state(states) self.lab_logger().info( f"更新了资源属性 {plr_resource}[{tree.root_node.res_content.uuid}] 及其子节点 {len(original_instance.get_all_children())} 个" @@ -879,7 +915,7 @@ class BaseROS2DeviceNode(Node, Generic[T]): action_type, action_name, execute_callback=self._create_execute_callback(action_name, action_value_mapping), - callback_group=ReentrantCallbackGroup(), + callback_group=self.callback_group, ) self.lab_logger().trace(f"发布动作: {action_name}, 类型: {str_action_type}") @@ -1500,7 +1536,7 @@ class ROS2DeviceNode: asyncio.set_event_loop(loop) loop.run_forever() - ROS2DeviceNode._loop_thread = threading.Thread(target=run_event_loop, daemon=True, name="ROS2DeviceNode") + ROS2DeviceNode._loop_thread = threading.Thread(target=run_event_loop, daemon=True, name="ROS2DeviceNodeLoop") ROS2DeviceNode._loop_thread.start() logger.info(f"循环线程已启动") diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index f40e0cbb..43d16e8d 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -285,7 +285,7 @@ class HostNode(BaseROS2DeviceNode): # 创建定时器,定期发现设备 self._discovery_timer = self.create_timer( - discovery_interval, self._discovery_devices_callback, callback_group=ReentrantCallbackGroup() + discovery_interval, self._discovery_devices_callback, callback_group=self.callback_group ) # 添加ping-pong相关属性 @@ -494,7 +494,7 @@ class HostNode(BaseROS2DeviceNode): if len(init_new_res) > 1: # 一个物料,多个子节点 init_new_res = [init_new_res] resources: List[Resource] | List[List[Resource]] = init_new_res # initialize_resource已经返回list[dict] - device_ids = [device_id] + device_ids = [device_id.split("/")[-1]] bind_parent_id = [res_creation_input["parent"]] bind_location = [bind_locations] other_calling_param = [ @@ -618,7 +618,7 @@ class HostNode(BaseROS2DeviceNode): topic, lambda msg, d=device_id, p=property_name: self.property_callback(msg, d, p), 1, - callback_group=ReentrantCallbackGroup(), + callback_group=self.callback_group, ) # 标记为已订阅 self._subscribed_topics.add(topic) @@ -829,37 +829,37 @@ class HostNode(BaseROS2DeviceNode): def _init_host_service(self): self._resource_services: Dict[str, Service] = { "resource_add": self.create_service( - ResourceAdd, "/resources/add", self._resource_add_callback, callback_group=ReentrantCallbackGroup() + ResourceAdd, "/resources/add", self._resource_add_callback, callback_group=self.callback_group ), "resource_get": self.create_service( - SerialCommand, "/resources/get", self._resource_get_callback, callback_group=ReentrantCallbackGroup() + SerialCommand, "/resources/get", self._resource_get_callback, callback_group=self.callback_group ), "resource_delete": self.create_service( ResourceDelete, "/resources/delete", self._resource_delete_callback, - callback_group=ReentrantCallbackGroup(), + callback_group=self.callback_group, ), "resource_update": self.create_service( ResourceUpdate, "/resources/update", self._resource_update_callback, - callback_group=ReentrantCallbackGroup(), + callback_group=self.callback_group, ), "resource_list": self.create_service( - ResourceList, "/resources/list", self._resource_list_callback, callback_group=ReentrantCallbackGroup() + ResourceList, "/resources/list", self._resource_list_callback, callback_group=self.callback_group ), "node_info_update": self.create_service( SerialCommand, "/node_info_update", self._node_info_update_callback, - callback_group=ReentrantCallbackGroup(), + callback_group=self.callback_group, ), "c2s_update_resource_tree": self.create_service( SerialCommand, "/c2s_update_resource_tree", self._resource_tree_update_callback, - callback_group=ReentrantCallbackGroup(), + callback_group=self.callback_group, ), } diff --git a/unilabos/ros/nodes/presets/workstation.py b/unilabos/ros/nodes/presets/workstation.py index dc1175d6..af1afab5 100644 --- a/unilabos/ros/nodes/presets/workstation.py +++ b/unilabos/ros/nodes/presets/workstation.py @@ -194,7 +194,7 @@ class ROS2WorkstationNode(BaseROS2DeviceNode): action_type, action_name, execute_callback=self._create_protocol_execute_callback(action_name, protocol_steps_generator), - callback_group=ReentrantCallbackGroup(), + callback_group=self.callback_group, ) self.lab_logger().trace(f"发布动作: {action_name}, 类型: {str_action_type}") return diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index cd533aab..c958fe7b 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -42,7 +42,9 @@ class ResourceDictPosition(BaseModel): rotation: ResourceDictPositionObject = Field( description="Resource rotation", default_factory=ResourceDictPositionObject ) - cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"] = Field(description="Cross section type", default="rectangle") + cross_section_type: Literal["rectangle", "circle", "rounded_rectangle"] = Field( + description="Cross section type", default="rectangle" + ) # 统一的资源字典模型,parent 自动序列化为 parent_uuid,children 不序列化 @@ -51,7 +53,9 @@ class ResourceDict(BaseModel): uuid: str = Field(description="Resource UUID") name: str = Field(description="Resource name") description: str = Field(description="Resource description", default="") - resource_schema: Dict[str, Any] = Field(description="Resource schema", default_factory=dict, serialization_alias="schema", validation_alias="schema") + resource_schema: Dict[str, Any] = Field( + description="Resource schema", default_factory=dict, serialization_alias="schema", validation_alias="schema" + ) model: Dict[str, Any] = Field(description="Resource model", default_factory=dict) icon: str = Field(description="Resource icon", default="") parent_uuid: Optional["str"] = Field(description="Parent resource uuid", default=None) # 先设定parent_uuid @@ -62,6 +66,7 @@ class ResourceDict(BaseModel): pose: ResourceDictPosition = Field(description="Resource position", default_factory=ResourceDictPosition) config: Dict[str, Any] = Field(description="Resource configuration") data: Dict[str, Any] = Field(description="Resource data") + extra: Dict[str, Any] = Field(description="Extra data") @field_serializer("parent_uuid") def _serialize_parent(self, parent_uuid: Optional["ResourceDict"]): @@ -138,6 +143,8 @@ class ResourceDictInstance(object): content["config"] = {} if not content.get("data"): content["data"] = {} + if not content.get("extra"): # MagicCode + content["extra"] = {} if "pose" not in content: content["pose"] = content.get("position", {}) return ResourceDictInstance(ResourceDict.model_validate(content)) @@ -311,28 +318,36 @@ class ResourceTreeSet(object): "plate": "plate", "well": "well", "deck": "deck", + "tip_rack": "tip_rack", + "tip_spot": "tip_spot", + "tube": "tube", + "bottle_carrier": "bottle_carrier", } if source in replace_info: return replace_info[source] else: print("转换pylabrobot的时候,出现未知类型", source) - return "container" + return source - def build_uuid_mapping(res: "PLRResource", uuid_list: list): - """递归构建uuid映射字典""" + def build_uuid_mapping(res: "PLRResource", uuid_list: list, parent_uuid: Optional[str] = None): + """递归构建uuid和extra映射字典,返回(current_uuid, parent_uuid, extra)元组列表""" uid = getattr(res, "unilabos_uuid", "") if not uid: uid = str(uuid.uuid4()) res.unilabos_uuid = uid logger.warning(f"{res}没有uuid,请设置后再传入,默认填充{uid}!\n{traceback.format_exc()}") - uuid_list.append(uid) + + # 获取unilabos_extra,默认为空字典 + extra = getattr(res, "unilabos_extra", {}) + + uuid_list.append((uid, parent_uuid, extra)) for child in res.children: - build_uuid_mapping(child, uuid_list) + build_uuid_mapping(child, uuid_list, uid) def resource_plr_inner( d: dict, parent_resource: Optional[ResourceDict], states: dict, uuids: list ) -> ResourceDictInstance: - current_uuid = uuids.pop(0) + current_uuid, parent_uuid, extra = uuids.pop(0) raw_pos = ( {"x": d["location"]["x"], "y": d["location"]["y"], "z": d["location"]["z"]} @@ -355,13 +370,30 @@ class ResourceTreeSet(object): "uuid": current_uuid, "name": d["name"], "parent": parent_resource, # 直接传入 ResourceDict 对象 + "parent_uuid": parent_uuid, # 使用 parent_uuid 而不是 parent 对象 "type": replace_plr_type(d.get("category", "")), "class": d.get("class", ""), "position": pos, "pose": pos, - "config": {k: v for k, v in d.items() if k not in - ["name", "children", "parent_name", "location", "rotation", "size_x", "size_y", "size_z", "cross_section_type", "bottom_type"]}, + "config": { + k: v + for k, v in d.items() + if k + not in [ + "name", + "children", + "parent_name", + "location", + "rotation", + "size_x", + "size_y", + "size_z", + "cross_section_type", + "bottom_type", + ] + }, "data": states[d["name"]], + "extra": extra, } # 先转换为 ResourceDictInstance,获取其中的 ResourceDict @@ -379,7 +411,7 @@ class ResourceTreeSet(object): for resource in resources: # 构建uuid列表 uuid_list = [] - build_uuid_mapping(resource, uuid_list) + build_uuid_mapping(resource, uuid_list, getattr(resource.parent, "unilabos_uuid", None)) serialized_data = resource.serialize() all_states = resource.serialize_all_state() @@ -402,14 +434,15 @@ class ResourceTreeSet(object): import inspect # 类型映射 - TYPE_MAP = {"plate": "Plate", "well": "Well", "deck": "Deck"} + TYPE_MAP = {"plate": "Plate", "well": "Well", "deck": "Deck", "container": "RegularContainer"} - def collect_node_data(node: ResourceDictInstance, name_to_uuid: dict, all_states: dict): - """一次遍历收集 name_to_uuid 和 all_states""" + def collect_node_data(node: ResourceDictInstance, name_to_uuid: dict, all_states: dict, name_to_extra: dict): + """一次遍历收集 name_to_uuid, all_states 和 name_to_extra""" name_to_uuid[node.res_content.name] = node.res_content.uuid all_states[node.res_content.name] = node.res_content.data + name_to_extra[node.res_content.name] = node.res_content.extra for child in node.children: - collect_node_data(child, name_to_uuid, all_states) + collect_node_data(child, name_to_uuid, all_states, name_to_extra) def node_to_plr_dict(node: ResourceDictInstance, has_model: bool): """转换节点为 PLR 字典格式""" @@ -419,6 +452,7 @@ class ResourceTreeSet(object): logger.warning(f"未知类型 {res.type}") d = { + **res.config, "name": res.name, "type": res.config.get("type", plr_type), "size_x": res.config.get("size_x", 0), @@ -434,33 +468,35 @@ class ResourceTreeSet(object): "category": res.config.get("category", plr_type), "children": [node_to_plr_dict(child, has_model) for child in node.children], "parent_name": res.parent_instance_name, - **res.config, } if has_model: d["model"] = res.config.get("model", None) return d plr_resources = [] - trees = [] tracker = DeviceNodeResourceTracker() for tree in self.trees: name_to_uuid: Dict[str, str] = {} all_states: Dict[str, Any] = {} - collect_node_data(tree.root_node, name_to_uuid, all_states) + name_to_extra: Dict[str, dict] = {} + collect_node_data(tree.root_node, name_to_uuid, all_states, name_to_extra) has_model = tree.root_node.res_content.type != "deck" plr_dict = node_to_plr_dict(tree.root_node, has_model) try: sub_cls = find_subclass(plr_dict["type"], PLRResource) if sub_cls is None: - raise ValueError(f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类。原始信息:{tree.root_node.res_content}") + raise ValueError( + f"无法找到类型 {plr_dict['type']} 对应的 PLR 资源类。原始信息:{tree.root_node.res_content}" + ) spec = inspect.signature(sub_cls) if "category" not in spec.parameters: plr_dict.pop("category", None) plr_resource = sub_cls.deserialize(plr_dict, allow_marshal=True) plr_resource.load_all_state(all_states) - # 使用 DeviceNodeResourceTracker 设置 UUID + # 使用 DeviceNodeResourceTracker 设置 UUID 和 Extra tracker.loop_set_uuid(plr_resource, name_to_uuid) + tracker.loop_set_extra(plr_resource, name_to_extra) plr_resources.append(plr_resource) except Exception as e: @@ -802,6 +838,20 @@ class DeviceNodeResourceTracker(object): else: setattr(resource, "unilabos_uuid", new_uuid) + @staticmethod + def set_resource_extra(resource, extra: dict): + """ + 设置资源的 extra,统一处理 dict 和 instance 两种类型 + + Args: + resource: 资源对象(dict或实例) + extra: extra字典值 + """ + if isinstance(resource, dict): + resource["extra"] = extra + else: + setattr(resource, "unilabos_extra", extra) + def _traverse_and_process(self, resource, process_func) -> int: """ 递归遍历资源树,对每个节点执行处理函数 @@ -850,6 +900,29 @@ class DeviceNodeResourceTracker(object): return self._traverse_and_process(resource, process) + def loop_set_extra(self, resource, name_to_extra_map: Dict[str, dict]) -> int: + """ + 递归遍历资源树,根据 name 设置所有节点的 extra + + Args: + resource: 资源对象(可以是dict或实例) + name_to_extra_map: name到extra的映射字典,{name: extra} + + Returns: + 更新的资源数量 + """ + + def process(res): + resource_name = self._get_resource_attr(res, "name") + if resource_name and resource_name in name_to_extra_map: + extra = name_to_extra_map[resource_name] + self.set_resource_extra(res, extra) + logger.debug(f"设置资源Extra: {resource_name} -> {extra}") + return 1 + return 0 + + return self._traverse_and_process(resource, process) + def loop_update_uuid(self, resource, uuid_map: Dict[str, str]) -> int: """ 递归遍历资源树,更新所有节点的uuid @@ -892,7 +965,9 @@ class DeviceNodeResourceTracker(object): if current_uuid: old = self.uuid_to_resources.get(current_uuid) self.uuid_to_resources[current_uuid] = res - logger.debug(f"收集资源UUID映射: {current_uuid} -> {res} {'' if old is None else f'(覆盖旧值: {old})'}") + logger.debug( + f"收集资源UUID映射: {current_uuid} -> {res} {'' if old is None else f'(覆盖旧值: {old})'}" + ) return 0 self._traverse_and_process(resource, process)