diff --git a/README.md b/README.md index b3256575..0c1d9b11 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,11 @@ conda env update --file unilabos-[YOUR_OS].yml -n 环境名 # 现阶段,需要安装 `unilabos_msgs` 包 # 可以前往 Release 页面下载系统对应的包进行安装 -conda install ros-humble-unilabos-msgs-0.8.0-xxxxx.tar.bz2 +conda install ros-humble-unilabos-msgs-0.9.0-xxxxx.tar.bz2 # 安装PyLabRobot等前置 -git clone https://github.com/PyLabRobot/pylabrobot +git clone https://github.com/PyLabRobot/pylabrobot plr_repo +cd plr_repo pip install .[opentrons] ``` diff --git a/recipes/ros-humble-unilabos-msgs/recipe.yaml b/recipes/ros-humble-unilabos-msgs/recipe.yaml index db9c3ede..b6997113 100644 --- a/recipes/ros-humble-unilabos-msgs/recipe.yaml +++ b/recipes/ros-humble-unilabos-msgs/recipe.yaml @@ -1,6 +1,6 @@ package: name: ros-humble-unilabos-msgs - version: 0.8.0 + version: 0.9.0 source: path: ../../unilabos_msgs folder: ros-humble-unilabos-msgs/src/work diff --git a/recipes/unilabos/recipe.yaml b/recipes/unilabos/recipe.yaml index 4fec1c02..4840bd65 100644 --- a/recipes/unilabos/recipe.yaml +++ b/recipes/unilabos/recipe.yaml @@ -1,6 +1,6 @@ package: name: unilabos - version: "0.8.0" + version: "0.9.0" source: path: ../.. diff --git a/setup.py b/setup.py index 5e29ee8b..5c06a7d8 100644 --- a/setup.py +++ b/setup.py @@ -4,7 +4,7 @@ package_name = 'unilabos' setup( name=package_name, - version='0.8.0', + version='0.9.0', packages=find_packages(), include_package_data=True, install_requires=['setuptools'], diff --git a/test/commands/resource_add.md b/test/commands/resource_add.md new file mode 100644 index 00000000..9d5fe38f --- /dev/null +++ b/test/commands/resource_add.md @@ -0,0 +1,5 @@ +使用plr_test.json启动,将Well加入Plate中 + +```bash +ros2 action send_goal /devices/host_node/add_resource_from_outer unilabos_msgs/action/_resource_create_from_outer/ResourceCreateFromOuter "{ resources: [ { 'category': '', 'children': [], 'config': { 'type': 'Well', 'size_x': 6.86, 'size_y': 6.86, 'size_z': 10.67, 'rotation': { 'x': 0, 'y': 0, 'z': 0, 'type': 'Rotation' }, 'category': 'well', 'model': null, 'max_volume': 360, 'material_z_thickness': 0.5, 'compute_volume_from_height': null, 'compute_height_from_volume': null, 'bottom_type': 'flat', 'cross_section_type': 'circle' }, 'data': { 'liquids': [], 'pending_liquids': [], 'liquid_history': [] }, 'id': 'plate_well_11_7', 'name': 'plate_well_11_7', 'pose': { 'orientation': { 'w': 1.0, 'x': 0.0, 'y': 0.0, 'z': 0.0 }, 'position': { 'x': 0.0, 'y': 0.0, 'z': 0.0 } }, 'sample_id': '', 'parent': 'plate', 'type': 'device' } ], device_ids: [ 'PLR_STATION' ], bind_parent_ids: [ 'plate' ], bind_locations: [ { 'x': 0.0, 'y': 0.0, 'z': 0.0 } ], other_calling_params: [ '{}' ] }" +``` \ No newline at end of file diff --git a/test/experiments/plr_test.json b/test/experiments/plr_test.json index c60f1b54..7fe1e3e3 100644 --- a/test/experiments/plr_test.json +++ b/test/experiments/plr_test.json @@ -6679,8 +6679,7 @@ "plate_well_11_3", "plate_well_11_4", "plate_well_11_5", - "plate_well_11_6", - "plate_well_11_7" + "plate_well_11_6" ], "parent": "deck", "type": "device", @@ -10508,45 +10507,6 @@ "pending_liquids": [], "liquid_history": [] } - }, - { - "id": "plate_well_11_7", - "name": "plate_well_11_7", - "sample_id": null, - "children": [], - "parent": "plate", - "type": "device", - "class": "", - "position": { - "x": 109.87, - "y": 7.77, - "z": 3.03 - }, - "config": { - "type": "Well", - "size_x": 6.86, - "size_y": 6.86, - "size_z": 10.67, - "rotation": { - "x": 0, - "y": 0, - "z": 0, - "type": "Rotation" - }, - "category": "well", - "model": null, - "max_volume": 360, - "material_z_thickness": 0.5, - "compute_volume_from_height": null, - "compute_height_from_volume": null, - "bottom_type": "flat", - "cross_section_type": "circle" - }, - "data": { - "liquids": [], - "pending_liquids": [], - "liquid_history": [] - } } ], "links": [] diff --git a/test/experiments/plr_test_converted.json b/test/experiments/plr_test_converted.json new file mode 100644 index 00000000..533d99c3 --- /dev/null +++ b/test/experiments/plr_test_converted.json @@ -0,0 +1,9590 @@ +{ + "nodes": [ + { + "id": "PLR_STATION", + "name": "PLR_LH_TEST", + "parent": null, + "type": "device", + "class": "liquid_handler", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "data": { + "children": [ + { + "_resource_child_name": "deck", + "_resource_type": "pylabrobot.resources.opentrons.deck:OTDeck" + } + ], + "backend": { + "type": "LiquidHandlerRvizBackend" + } + } + }, + "data": {}, + "children": [ + "deck" + ] + }, + { + "id": "deck", + "name": "deck", + "sample_id": null, + "children": [ + "tip_rack", + "plate_well" + ], + "parent": "PLR_STATION", + "type": "deck", + "class": "OTDeck", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "OTDeck", + "with_trash": false, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + } + }, + "data": {} + }, + { + "id": "tip_rack", + "name": "tip_rack", + "sample_id": null, + "children": [ + "tip_rack_A1", + "tip_rack_B1", + "tip_rack_C1", + "tip_rack_D1", + "tip_rack_E1", + "tip_rack_F1", + "tip_rack_G1", + "tip_rack_H1", + "tip_rack_A2", + "tip_rack_B2", + "tip_rack_C2", + "tip_rack_D2", + "tip_rack_E2", + "tip_rack_F2", + "tip_rack_G2", + "tip_rack_H2", + "tip_rack_A3", + "tip_rack_B3", + "tip_rack_C3", + "tip_rack_D3", + "tip_rack_E3", + "tip_rack_F3", + "tip_rack_G3", + "tip_rack_H3", + "tip_rack_A4", + "tip_rack_B4", + "tip_rack_C4", + "tip_rack_D4", + "tip_rack_E4", + "tip_rack_F4", + "tip_rack_G4", + "tip_rack_H4", + "tip_rack_A5", + "tip_rack_B5", + "tip_rack_C5", + "tip_rack_D5", + "tip_rack_E5", + "tip_rack_F5", + "tip_rack_G5", + "tip_rack_H5", + "tip_rack_A6", + "tip_rack_B6", + "tip_rack_C6", + "tip_rack_D6", + "tip_rack_E6", + "tip_rack_F6", + "tip_rack_G6", + "tip_rack_H6", + "tip_rack_A7", + "tip_rack_B7", + "tip_rack_C7", + "tip_rack_D7", + "tip_rack_E7", + "tip_rack_F7", + "tip_rack_G7", + "tip_rack_H7", + "tip_rack_A8", + "tip_rack_B8", + "tip_rack_C8", + "tip_rack_D8", + "tip_rack_E8", + "tip_rack_F8", + "tip_rack_G8", + "tip_rack_H8", + "tip_rack_A9", + "tip_rack_B9", + "tip_rack_C9", + "tip_rack_D9", + "tip_rack_E9", + "tip_rack_F9", + "tip_rack_G9", + "tip_rack_H9", + "tip_rack_A10", + "tip_rack_B10", + "tip_rack_C10", + "tip_rack_D10", + "tip_rack_E10", + "tip_rack_F10", + "tip_rack_G10", + "tip_rack_H10", + "tip_rack_A11", + "tip_rack_B11", + "tip_rack_C11", + "tip_rack_D11", + "tip_rack_E11", + "tip_rack_F11", + "tip_rack_G11", + "tip_rack_H11", + "tip_rack_A12", + "tip_rack_B12", + "tip_rack_C12", + "tip_rack_D12", + "tip_rack_E12", + "tip_rack_F12", + "tip_rack_G12", + "tip_rack_H12" + ], + "parent": "deck", + "type": "plate", + "class": "opentrons_96_filtertiprack_1000ul", + "position": { + "x": 0, + "y": 0, + "z": 69 + }, + "config": { + "type": "TipRack", + "size_x": 122.4, + "size_y": 82.6, + "size_z": 20.0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_rack", + "model": "HTF", + "ordering": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ] + }, + "data": {} + }, + { + "id": "tip_rack_A1", + "name": "tip_rack_A1", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 7.2, + "y": 68.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_B1", + "name": "tip_rack_B1", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 7.2, + "y": 59.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_C1", + "name": "tip_rack_C1", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 7.2, + "y": 50.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_D1", + "name": "tip_rack_D1", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 7.2, + "y": 41.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_E1", + "name": "tip_rack_E1", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 7.2, + "y": 32.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_F1", + "name": "tip_rack_F1", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 7.2, + "y": 23.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_G1", + "name": "tip_rack_G1", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 7.2, + "y": 14.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_H1", + "name": "tip_rack_H1", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 7.2, + "y": 5.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_A2", + "name": "tip_rack_A2", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 16.2, + "y": 68.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_B2", + "name": "tip_rack_B2", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 16.2, + "y": 59.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_C2", + "name": "tip_rack_C2", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 16.2, + "y": 50.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_D2", + "name": "tip_rack_D2", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 16.2, + "y": 41.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_E2", + "name": "tip_rack_E2", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 16.2, + "y": 32.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_F2", + "name": "tip_rack_F2", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 16.2, + "y": 23.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_G2", + "name": "tip_rack_G2", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 16.2, + "y": 14.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_H2", + "name": "tip_rack_H2", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 16.2, + "y": 5.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_A3", + "name": "tip_rack_A3", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 25.2, + "y": 68.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_B3", + "name": "tip_rack_B3", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 25.2, + "y": 59.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_C3", + "name": "tip_rack_C3", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 25.2, + "y": 50.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_D3", + "name": "tip_rack_D3", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 25.2, + "y": 41.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_E3", + "name": "tip_rack_E3", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 25.2, + "y": 32.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_F3", + "name": "tip_rack_F3", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 25.2, + "y": 23.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_G3", + "name": "tip_rack_G3", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 25.2, + "y": 14.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_H3", + "name": "tip_rack_H3", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 25.2, + "y": 5.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_A4", + "name": "tip_rack_A4", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 34.2, + "y": 68.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_B4", + "name": "tip_rack_B4", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 34.2, + "y": 59.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_C4", + "name": "tip_rack_C4", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 34.2, + "y": 50.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_D4", + "name": "tip_rack_D4", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 34.2, + "y": 41.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_E4", + "name": "tip_rack_E4", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 34.2, + "y": 32.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_F4", + "name": "tip_rack_F4", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 34.2, + "y": 23.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_G4", + "name": "tip_rack_G4", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 34.2, + "y": 14.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_H4", + "name": "tip_rack_H4", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 34.2, + "y": 5.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_A5", + "name": "tip_rack_A5", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 43.2, + "y": 68.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_B5", + "name": "tip_rack_B5", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 43.2, + "y": 59.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_C5", + "name": "tip_rack_C5", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 43.2, + "y": 50.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_D5", + "name": "tip_rack_D5", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 43.2, + "y": 41.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_E5", + "name": "tip_rack_E5", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 43.2, + "y": 32.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_F5", + "name": "tip_rack_F5", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 43.2, + "y": 23.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_G5", + "name": "tip_rack_G5", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 43.2, + "y": 14.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_H5", + "name": "tip_rack_H5", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 43.2, + "y": 5.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_A6", + "name": "tip_rack_A6", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 52.2, + "y": 68.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_B6", + "name": "tip_rack_B6", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 52.2, + "y": 59.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_C6", + "name": "tip_rack_C6", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 52.2, + "y": 50.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_D6", + "name": "tip_rack_D6", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 52.2, + "y": 41.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_E6", + "name": "tip_rack_E6", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 52.2, + "y": 32.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_F6", + "name": "tip_rack_F6", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 52.2, + "y": 23.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_G6", + "name": "tip_rack_G6", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 52.2, + "y": 14.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_H6", + "name": "tip_rack_H6", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 52.2, + "y": 5.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_A7", + "name": "tip_rack_A7", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 61.2, + "y": 68.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_B7", + "name": "tip_rack_B7", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 61.2, + "y": 59.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_C7", + "name": "tip_rack_C7", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 61.2, + "y": 50.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_D7", + "name": "tip_rack_D7", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 61.2, + "y": 41.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_E7", + "name": "tip_rack_E7", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 61.2, + "y": 32.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_F7", + "name": "tip_rack_F7", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 61.2, + "y": 23.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_G7", + "name": "tip_rack_G7", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 61.2, + "y": 14.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_H7", + "name": "tip_rack_H7", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 61.2, + "y": 5.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_A8", + "name": "tip_rack_A8", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 70.2, + "y": 68.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_B8", + "name": "tip_rack_B8", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 70.2, + "y": 59.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_C8", + "name": "tip_rack_C8", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 70.2, + "y": 50.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_D8", + "name": "tip_rack_D8", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 70.2, + "y": 41.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_E8", + "name": "tip_rack_E8", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 70.2, + "y": 32.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_F8", + "name": "tip_rack_F8", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 70.2, + "y": 23.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_G8", + "name": "tip_rack_G8", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 70.2, + "y": 14.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_H8", + "name": "tip_rack_H8", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 70.2, + "y": 5.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_A9", + "name": "tip_rack_A9", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 79.2, + "y": 68.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_B9", + "name": "tip_rack_B9", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 79.2, + "y": 59.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_C9", + "name": "tip_rack_C9", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 79.2, + "y": 50.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_D9", + "name": "tip_rack_D9", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 79.2, + "y": 41.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_E9", + "name": "tip_rack_E9", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 79.2, + "y": 32.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_F9", + "name": "tip_rack_F9", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 79.2, + "y": 23.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_G9", + "name": "tip_rack_G9", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 79.2, + "y": 14.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_H9", + "name": "tip_rack_H9", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 79.2, + "y": 5.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_A10", + "name": "tip_rack_A10", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 88.2, + "y": 68.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_B10", + "name": "tip_rack_B10", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 88.2, + "y": 59.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_C10", + "name": "tip_rack_C10", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 88.2, + "y": 50.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_D10", + "name": "tip_rack_D10", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 88.2, + "y": 41.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_E10", + "name": "tip_rack_E10", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 88.2, + "y": 32.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_F10", + "name": "tip_rack_F10", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 88.2, + "y": 23.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_G10", + "name": "tip_rack_G10", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 88.2, + "y": 14.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_H10", + "name": "tip_rack_H10", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 88.2, + "y": 5.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_A11", + "name": "tip_rack_A11", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 97.2, + "y": 68.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_B11", + "name": "tip_rack_B11", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 97.2, + "y": 59.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_C11", + "name": "tip_rack_C11", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 97.2, + "y": 50.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_D11", + "name": "tip_rack_D11", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 97.2, + "y": 41.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_E11", + "name": "tip_rack_E11", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 97.2, + "y": 32.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_F11", + "name": "tip_rack_F11", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 97.2, + "y": 23.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_G11", + "name": "tip_rack_G11", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 97.2, + "y": 14.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_H11", + "name": "tip_rack_H11", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 97.2, + "y": 5.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_A12", + "name": "tip_rack_A12", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 106.2, + "y": 68.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_B12", + "name": "tip_rack_B12", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 106.2, + "y": 59.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_C12", + "name": "tip_rack_C12", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 106.2, + "y": 50.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_D12", + "name": "tip_rack_D12", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 106.2, + "y": 41.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_E12", + "name": "tip_rack_E12", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 106.2, + "y": 32.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_F12", + "name": "tip_rack_F12", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 106.2, + "y": 23.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_G12", + "name": "tip_rack_G12", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 106.2, + "y": 14.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "tip_rack_H12", + "name": "tip_rack_H12", + "sample_id": null, + "children": [], + "parent": "tip_rack", + "type": "device", + "class": "", + "position": { + "x": 106.2, + "y": 5.3, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 9.0, + "size_y": 9.0, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + }, + "tip_state": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + }, + "pending_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + } + }, + { + "id": "plate_well", + "name": "plate_well", + "sample_id": null, + "children": [ + "plate_well_A1", + "plate_well_B1", + "plate_well_C1", + "plate_well_D1", + "plate_well_E1", + "plate_well_F1", + "plate_well_G1", + "plate_well_H1", + "plate_well_A2", + "plate_well_B2", + "plate_well_C2", + "plate_well_D2", + "plate_well_E2", + "plate_well_F2", + "plate_well_G2", + "plate_well_H2", + "plate_well_A3", + "plate_well_B3", + "plate_well_C3", + "plate_well_D3", + "plate_well_E3", + "plate_well_F3", + "plate_well_G3", + "plate_well_H3", + "plate_well_A4", + "plate_well_B4", + "plate_well_C4", + "plate_well_D4", + "plate_well_E4", + "plate_well_F4", + "plate_well_G4", + "plate_well_H4", + "plate_well_A5", + "plate_well_B5", + "plate_well_C5", + "plate_well_D5", + "plate_well_E5", + "plate_well_F5", + "plate_well_G5", + "plate_well_H5", + "plate_well_A6", + "plate_well_B6", + "plate_well_C6", + "plate_well_D6", + "plate_well_E6", + "plate_well_F6", + "plate_well_G6", + "plate_well_H6", + "plate_well_A7", + "plate_well_B7", + "plate_well_C7", + "plate_well_D7", + "plate_well_E7", + "plate_well_F7", + "plate_well_G7", + "plate_well_H7", + "plate_well_A8", + "plate_well_B8", + "plate_well_C8", + "plate_well_D8", + "plate_well_E8", + "plate_well_F8", + "plate_well_G8", + "plate_well_H8", + "plate_well_A9", + "plate_well_B9", + "plate_well_C9", + "plate_well_D9", + "plate_well_E9", + "plate_well_F9", + "plate_well_G9", + "plate_well_H9", + "plate_well_A10", + "plate_well_B10", + "plate_well_C10", + "plate_well_D10", + "plate_well_E10", + "plate_well_F10", + "plate_well_G10", + "plate_well_H10", + "plate_well_A11", + "plate_well_B11", + "plate_well_C11", + "plate_well_D11", + "plate_well_E11", + "plate_well_F11", + "plate_well_G11", + "plate_well_H11", + "plate_well_A12", + "plate_well_B12", + "plate_well_C12", + "plate_well_D12", + "plate_well_E12", + "plate_well_F12", + "plate_well_G12" + ], + "parent": "deck", + "type": "plate", + "class": "nest_96_wellplate_2ml_deep", + "position": { + "x": 265.0, + "y": 0, + "z": 69 + }, + "config": { + "type": "Plate", + "size_x": 127.76, + "size_y": 85.48, + "size_z": 14.2, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "plate", + "model": "Cor_96_wellplate_360ul_Fb", + "ordering": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ] + }, + "data": {} + }, + { + "id": "plate_well_A1", + "name": "plate_well_A1", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 10.87, + "y": 70.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_B1", + "name": "plate_well_B1", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 10.87, + "y": 61.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_C1", + "name": "plate_well_C1", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 10.87, + "y": 52.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_D1", + "name": "plate_well_D1", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 10.87, + "y": 43.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_E1", + "name": "plate_well_E1", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 10.87, + "y": 34.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_F1", + "name": "plate_well_F1", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 10.87, + "y": 25.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_G1", + "name": "plate_well_G1", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 10.87, + "y": 16.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_H1", + "name": "plate_well_H1", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 10.87, + "y": 7.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_A2", + "name": "plate_well_A2", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 19.87, + "y": 70.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_B2", + "name": "plate_well_B2", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 19.87, + "y": 61.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_C2", + "name": "plate_well_C2", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 19.87, + "y": 52.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_D2", + "name": "plate_well_D2", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 19.87, + "y": 43.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_E2", + "name": "plate_well_E2", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 19.87, + "y": 34.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_F2", + "name": "plate_well_F2", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 19.87, + "y": 25.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_G2", + "name": "plate_well_G2", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 19.87, + "y": 16.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_H2", + "name": "plate_well_H2", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 19.87, + "y": 7.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_A3", + "name": "plate_well_A3", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 28.87, + "y": 70.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_B3", + "name": "plate_well_B3", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 28.87, + "y": 61.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_C3", + "name": "plate_well_C3", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 28.87, + "y": 52.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_D3", + "name": "plate_well_D3", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 28.87, + "y": 43.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_E3", + "name": "plate_well_E3", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 28.87, + "y": 34.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_F3", + "name": "plate_well_F3", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 28.87, + "y": 25.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_G3", + "name": "plate_well_G3", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 28.87, + "y": 16.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_H3", + "name": "plate_well_H3", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 28.87, + "y": 7.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_A4", + "name": "plate_well_A4", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 37.87, + "y": 70.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_B4", + "name": "plate_well_B4", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 37.87, + "y": 61.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_C4", + "name": "plate_well_C4", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 37.87, + "y": 52.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_D4", + "name": "plate_well_D4", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 37.87, + "y": 43.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_E4", + "name": "plate_well_E4", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 37.87, + "y": 34.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_F4", + "name": "plate_well_F4", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 37.87, + "y": 25.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_G4", + "name": "plate_well_G4", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 37.87, + "y": 16.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_H4", + "name": "plate_well_H4", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 37.87, + "y": 7.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_A5", + "name": "plate_well_A5", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 46.87, + "y": 70.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_B5", + "name": "plate_well_B5", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 46.87, + "y": 61.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_C5", + "name": "plate_well_C5", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 46.87, + "y": 52.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_D5", + "name": "plate_well_D5", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 46.87, + "y": 43.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_E5", + "name": "plate_well_E5", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 46.87, + "y": 34.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_F5", + "name": "plate_well_F5", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 46.87, + "y": 25.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_G5", + "name": "plate_well_G5", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 46.87, + "y": 16.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_H5", + "name": "plate_well_H5", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 46.87, + "y": 7.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_A6", + "name": "plate_well_A6", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 55.87, + "y": 70.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_B6", + "name": "plate_well_B6", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 55.87, + "y": 61.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_C6", + "name": "plate_well_C6", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 55.87, + "y": 52.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_D6", + "name": "plate_well_D6", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 55.87, + "y": 43.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_E6", + "name": "plate_well_E6", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 55.87, + "y": 34.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_F6", + "name": "plate_well_F6", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 55.87, + "y": 25.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_G6", + "name": "plate_well_G6", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 55.87, + "y": 16.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_H6", + "name": "plate_well_H6", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 55.87, + "y": 7.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_A7", + "name": "plate_well_A7", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 64.87, + "y": 70.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_B7", + "name": "plate_well_B7", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 64.87, + "y": 61.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_C7", + "name": "plate_well_C7", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 64.87, + "y": 52.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_D7", + "name": "plate_well_D7", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 64.87, + "y": 43.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_E7", + "name": "plate_well_E7", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 64.87, + "y": 34.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_F7", + "name": "plate_well_F7", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 64.87, + "y": 25.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_G7", + "name": "plate_well_G7", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 64.87, + "y": 16.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_H7", + "name": "plate_well_H7", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 64.87, + "y": 7.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_A8", + "name": "plate_well_A8", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 73.87, + "y": 70.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_B8", + "name": "plate_well_B8", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 73.87, + "y": 61.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_C8", + "name": "plate_well_C8", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 73.87, + "y": 52.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_D8", + "name": "plate_well_D8", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 73.87, + "y": 43.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_E8", + "name": "plate_well_E8", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 73.87, + "y": 34.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_F8", + "name": "plate_well_F8", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 73.87, + "y": 25.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_G8", + "name": "plate_well_G8", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 73.87, + "y": 16.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_H8", + "name": "plate_well_H8", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 73.87, + "y": 7.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_A9", + "name": "plate_well_A9", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 82.87, + "y": 70.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_B9", + "name": "plate_well_B9", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 82.87, + "y": 61.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_C9", + "name": "plate_well_C9", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 82.87, + "y": 52.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_D9", + "name": "plate_well_D9", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 82.87, + "y": 43.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_E9", + "name": "plate_well_E9", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 82.87, + "y": 34.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_F9", + "name": "plate_well_F9", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 82.87, + "y": 25.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_G9", + "name": "plate_well_G9", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 82.87, + "y": 16.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_H9", + "name": "plate_well_H9", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 82.87, + "y": 7.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_A10", + "name": "plate_well_A10", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 91.87, + "y": 70.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_B10", + "name": "plate_well_B10", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 91.87, + "y": 61.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_C10", + "name": "plate_well_C10", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 91.87, + "y": 52.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_D10", + "name": "plate_well_D10", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 91.87, + "y": 43.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_E10", + "name": "plate_well_E10", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 91.87, + "y": 34.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_F10", + "name": "plate_well_F10", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 91.87, + "y": 25.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_G10", + "name": "plate_well_G10", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 91.87, + "y": 16.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_H10", + "name": "plate_well_H10", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 91.87, + "y": 7.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_A11", + "name": "plate_well_A11", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 100.87, + "y": 70.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_B11", + "name": "plate_well_B11", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 100.87, + "y": 61.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_C11", + "name": "plate_well_C11", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 100.87, + "y": 52.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_D11", + "name": "plate_well_D11", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 100.87, + "y": 43.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_E11", + "name": "plate_well_E11", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 100.87, + "y": 34.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_F11", + "name": "plate_well_F11", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 100.87, + "y": 25.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_G11", + "name": "plate_well_G11", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 100.87, + "y": 16.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_H11", + "name": "plate_well_H11", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 100.87, + "y": 7.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_A12", + "name": "plate_well_A12", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 109.87, + "y": 70.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_B12", + "name": "plate_well_B12", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 109.87, + "y": 61.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_C12", + "name": "plate_well_C12", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 109.87, + "y": 52.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_D12", + "name": "plate_well_D12", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 109.87, + "y": 43.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_E12", + "name": "plate_well_E12", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 109.87, + "y": 34.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_F12", + "name": "plate_well_F12", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 109.87, + "y": 25.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + }, + { + "id": "plate_well_G12", + "name": "plate_well_G12", + "sample_id": null, + "children": [], + "parent": "plate_well", + "type": "device", + "class": "", + "position": { + "x": 109.87, + "y": 16.77, + "z": 3.03 + }, + "config": { + "type": "Well", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "well", + "model": null, + "max_volume": 360, + "material_z_thickness": 0.5, + "compute_volume_from_height": null, + "compute_height_from_volume": null, + "bottom_type": "flat", + "cross_section_type": "circle" + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + } + ], + "links": [] +} \ No newline at end of file diff --git a/test/experiments/test.json b/test/experiments/test.json index 07b802cd..ac5e5043 100644 --- a/test/experiments/test.json +++ b/test/experiments/test.json @@ -4,13 +4,14 @@ "id": "Gripper1", "name": "假夹爪", "children": [ + "Plate1" ], "parent": null, "type": "device", "class": "gripper.mock", "position": { - "x": 620.6111111111111, - "y": 171, + "x": 0, + "y": 0, "z": 0 }, "config": { @@ -23,18 +24,120 @@ "name": "Plate1", "children": [ ], - "parent": null, + "parent": "Gripper1", "type": "plate", - "class": "nest_96_wellplate_2ml_deep", + "class": "nest_96_wellplate_100ul_pcr_full_skirt", "position": { - "x": 620.6111111111111, - "y": 171, - "z": 0 + "x": 0, + "y": 0, + "z": 69 }, "config": { }, "data": { } + }, + { + "id": "ot_joint_publisher", + "name": "ot_joint_publisher", + "sample_id": null, + "children": [ + + ], + "parent": null, + "type": "device", + "class": "lh_joint_publisher", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "lh_id":"deck", + "joint_config": + { + "joint_names":[ + "first_joint", + "second_joint", + "third_joint", + "fourth_joint" + ], + "y":{ + "first_joint":{ + "factor":-1, + "offset":0.0 + } + }, + "x":{ + "second_joint":{ + "factor":-1, + "offset":0.0 + } + }, + "z":{ + "third_joint":{ + "factor":1, + "offset":0.0 + }, + "fourth_joint":{ + "factor":1, + "offset":0.0 + } + } + } + }, + "data": {} + }, + { + "id": "ot_joint_publisher", + "name": "ot_joint_publisher", + "sample_id": null, + "children": [ + + ], + "parent": null, + "type": "device", + "class": "lh_joint_publisher", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "lh_id":"deck", + "joint_config": + { + "joint_names":[ + "first_joint", + "second_joint", + "third_joint", + "fourth_joint" + ], + "y":{ + "first_joint":{ + "factor":-1, + "offset":0.0 + } + }, + "x":{ + "second_joint":{ + "factor":-1, + "offset":0.0 + } + }, + "z":{ + "third_joint":{ + "factor":1, + "offset":0.0 + }, + "fourth_joint":{ + "factor":1, + "offset":0.0 + } + } + } + }, + "data": {} } ], "links": [ diff --git a/test/experiments/test_copy.json b/test/experiments/test_copy.json new file mode 100644 index 00000000..b6ebb516 --- /dev/null +++ b/test/experiments/test_copy.json @@ -0,0 +1,135 @@ +{ + "nodes": [ + { + "id": "PLR_STATION", + "name": "PLR_LH_TEST", + "parent": null, + "type": "device", + "class": "liquid_handler", + "position": { + "x": 620.6111111111111, + "y": 171, + "z": 0 + }, + "config": { + "data": { + "children": [ + { + "_resource_child_name": "deck", + "_resource_type": "pylabrobot.resources.opentrons.deck:OTDeck" + } + ], + "backend": { + "type": "LiquidHandlerRvizBackend" + } + } + }, + "data": {}, + "children": [ + "deck" + ] + }, + { + "id": "deck", + "name": "deck", + "sample_id": null, + "children": [ + "teaching_carrier" + ], + "parent": "PLR_STATION", + "type": "deck", + "class": "OTDeck", + "position": { + "x": 0, + "y": 0, + "z": 0 + }, + "config": { + "type": "OTDeck", + "with_trash": false, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + } + }, + "data": {} + }, + + { + "id": "teaching_carrier", + "name": "teaching_carrier", + "sample_id": null, + "children": [ + "teaching_carrier_A1" + ], + "parent": "deck", + "type": "plate", + "class": "opentrons_96_filtertiprack_1000ul", + "position": { + "x": 0, + "y": 0, + "z": 69 + }, + "config": { + "type": "Resource", + "size_x": 127, + "size_y": 85, + "size_z": 0, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": null, + "model": null + }, + "data": {} + }, + { + "id": "teaching_carrier_A1", + "name": "teaching_carrier_A1", + "sample_id": null, + "children": [], + "parent": "teaching_carrier", + "type": "device", + "class": "", + "position": { + "x": 10.87, + "y": 70.77, + "z": 9.47 + }, + "config": { + "type": "TipSpot", + "size_x": 6.86, + "size_y": 6.86, + "size_z": 10.67, + "rotation": { + "x": 0, + "y": 0, + "z": 0, + "type": "Rotation" + }, + "category": "tip_spot", + "model": null, + "prototype_tip": { + "type": "Tip", + "total_tip_length": 39.2, + "has_filter": true, + "maximal_volume": 20.0, + "fitting_depth": 3.29 + } + }, + "data": { + "liquids": [], + "pending_liquids": [], + "liquid_history": [] + } + } + ], + "links": [ + + ] +} \ No newline at end of file diff --git a/unilabos-linux-64.yaml b/unilabos-linux-64.yaml index 7ce69c9b..3f5b91c6 100644 --- a/unilabos-linux-64.yaml +++ b/unilabos-linux-64.yaml @@ -59,3 +59,5 @@ dependencies: # ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo # ilab equipments # - ros-humble-unilabos-msgs + - pip: + - paho-mqtt \ No newline at end of file diff --git a/unilabos-osx-64.yaml b/unilabos-osx-64.yaml index 7e21a65d..38981f0a 100644 --- a/unilabos-osx-64.yaml +++ b/unilabos-osx-64.yaml @@ -59,3 +59,5 @@ dependencies: # ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo # ilab equipments # - ros-humble-unilabos-msgs + - pip: + - paho-mqtt \ No newline at end of file diff --git a/unilabos-osx-arm64.yaml b/unilabos-osx-arm64.yaml index 4c69fb90..05333a39 100644 --- a/unilabos-osx-arm64.yaml +++ b/unilabos-osx-arm64.yaml @@ -61,3 +61,5 @@ dependencies: # ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo # ilab equipments # - ros-humble-unilabos-msgs + - pip: + - paho-mqtt \ No newline at end of file diff --git a/unilabos-win64.yaml b/unilabos-win64.yaml index 03f010dc..2e26fa39 100644 --- a/unilabos-win64.yaml +++ b/unilabos-win64.yaml @@ -58,4 +58,6 @@ dependencies: - ros-humble-simulation # ignored because of NO python3.11 package in WIN64 # ros-humble-gazebo-ros // ignored because of the conflict with ign-gazebo # ilab equipments -# - ros-humble-unilabos-msgs + # ros-humble-unilabos-msgs + - pip: + - paho-mqtt \ No newline at end of file diff --git a/unilabos/app/backend.py b/unilabos/app/backend.py index 19cebff0..5bc4ad39 100644 --- a/unilabos/app/backend.py +++ b/unilabos/app/backend.py @@ -7,11 +7,13 @@ from unilabos.utils import logger def start_backend( backend: str, devices_config: dict = {}, - resources_config: dict = {}, + resources_config: list = [], graph=None, controllers_config: dict = {}, bridges=[], without_host: bool = False, + visual: str = "None", + resources_mesh_config: dict = {}, **kwargs ): if backend == "ros": @@ -29,7 +31,9 @@ def start_backend( backend_thread = threading.Thread( target=main if not without_host else slave, - args=(devices_config, resources_config, graph, controllers_config, bridges) + args=(devices_config, resources_config, graph, controllers_config, bridges, visual, resources_mesh_config), + name="backend_thread", + daemon=True, ) backend_thread.start() logger.info(f"Backend {backend} started.") diff --git a/unilabos/app/controler.py b/unilabos/app/controler.py index 391413f7..f58f53ab 100644 --- a/unilabos/app/controler.py +++ b/unilabos/app/controler.py @@ -29,6 +29,8 @@ def job_add(req: JobAddReq) -> JobData: req.data['action'] = action_name if action_name == "execute_command_from_outer": action_kwargs = {"command": json.dumps(action_kwargs)} + elif "command" in action_kwargs: + action_kwargs = action_kwargs["command"] print(f"job_add:{req.device_id} {action_name} {action_kwargs}") HostNode.get_instance().send_goal(req.device_id, action_name=action_name, action_kwargs=action_kwargs, goal_uuid=req.job_id) return JobData(jobId=req.job_id) diff --git a/unilabos/app/main.py b/unilabos/app/main.py index 89462822..ebee015e 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -1,19 +1,24 @@ import argparse +import asyncio +import json import os import signal import sys -import json -import yaml +import threading +import time from copy import deepcopy +import yaml + # 首先添加项目根目录到路径 current_dir = os.path.dirname(os.path.abspath(__file__)) -ilabos_dir = os.path.dirname(os.path.dirname(current_dir)) -if ilabos_dir not in sys.path: - sys.path.append(ilabos_dir) +unilabos_dir = os.path.dirname(os.path.dirname(current_dir)) +if unilabos_dir not in sys.path: + sys.path.append(unilabos_dir) from unilabos.config.config import load_config, BasicConfig, _update_config_from_env from unilabos.utils.banner_print import print_status, print_unilab_banner +from unilabos.device_mesh.resource_visalization import ResourceVisualization def parse_args(): @@ -65,12 +70,21 @@ def parse_args(): help="信息页web服务的启动端口", ) parser.add_argument( - "--open_browser", - type=bool, - default=True, - help="是否在启动时打开信息页", + "--disable_browser", + action='store_true', + help="是否在启动时关闭信息页", + ) + parser.add_argument( + "--2d_vis", + action='store_true', + help="是否在pylabrobot实例启动时,同时启动可视化", + ) + parser.add_argument( + "--visual", + choices=["rviz", "web", "disable"], + default="disable", + help="选择可视化工具: rviz, web", ) - return parser.parse_args() @@ -101,6 +115,7 @@ def main(): machine_name = os.popen("hostname").read().strip() machine_name = "".join([c if c.isalnum() or c == "_" else "_" for c in machine_name]) BasicConfig.machine_name = machine_name + BasicConfig.vis_2d_enable = args_dict["2d_vis"] from unilabos.resources.graphio import ( read_node_link_json, @@ -121,6 +136,7 @@ def main(): # 注册表 build_registry(args_dict["registry_path"]) + devices_and_resources = None if args_dict["graph"] is not None: import unilabos.resources.graphio as graph_res graph_res.physical_setup_graph = ( @@ -132,6 +148,7 @@ def main(): args_dict["resources_config"] = initialize_resources(list(deepcopy(devices_and_resources).values())) args_dict["devices_config"] = dict_to_nested_dict(deepcopy(devices_and_resources), devices_only=False) # args_dict["resources_config"] = dict_to_tree(devices_and_resources, devices_only=False) + args_dict["graph"] = graph_res.physical_setup_graph else: if args_dict["devices"] is None or args_dict["resources"] is None: @@ -166,9 +183,28 @@ def main(): signal.signal(signal.SIGINT, _exit) signal.signal(signal.SIGTERM, _exit) mqtt_client.start() - - start_backend(**args_dict) - start_server(port=args_dict.get("port", 8002), open_browser=args_dict.get("open_browser", False)) + args_dict["resources_mesh_config"] = {} + # web visiualize 2D + if args_dict["visual"] != "disable": + enable_rviz = args_dict["visual"] == "rviz" + if devices_and_resources is not None: + resource_visualization = ResourceVisualization(devices_and_resources, args_dict["resources_config"] ,enable_rviz=enable_rviz) + args_dict["resources_mesh_config"] = resource_visualization.resource_model + start_backend(**args_dict) + server_thread = threading.Thread(target=start_server, kwargs=dict( + open_browser=not args_dict["disable_browser"] + )) + server_thread.start() + asyncio.set_event_loop(asyncio.new_event_loop()) + resource_visualization.start() + while True: + time.sleep(1) + else: + start_backend(**args_dict) + start_server(open_browser=not args_dict["disable_browser"]) + else: + start_backend(**args_dict) + start_server(open_browser=not args_dict["disable_browser"]) if __name__ == "__main__": diff --git a/unilabos/app/mq.py b/unilabos/app/mq.py index a6123fb2..018c65cb 100644 --- a/unilabos/app/mq.py +++ b/unilabos/app/mq.py @@ -1,5 +1,6 @@ import json import time +import traceback import uuid import paho.mqtt.client as mqtt @@ -35,7 +36,8 @@ class MQTTClient: self.client.on_disconnect = self._on_disconnect def _on_log(self, client, userdata, level, buf): - logger.info(f"[MQTT] log: {buf}") + # logger.info(f"[MQTT] log: {buf}") + pass def _on_connect(self, client, userdata, flags, rc, properties=None): logger.info("[MQTT] Connected with result code " + str(rc)) @@ -54,6 +56,12 @@ class MQTTClient: logger.debug("Payload:", json.dumps(payload_json, indent=2, ensure_ascii=False)) if msg.topic == f"labs/{MQConfig.lab_id}/job/start/": logger.debug("job_add", type(payload_json), payload_json) + if "data" not in payload_json: + payload_json["data"] = {} + if "action" in payload_json: + payload_json["data"]["action"] = payload_json.pop("action") + if "action_kwargs" in payload_json: + payload_json["data"]["action_kwargs"] = payload_json.pop("action_kwargs") job_req = JobAddReq.model_validate(payload_json) data = job_add(job_req) return @@ -61,8 +69,10 @@ class MQTTClient: except json.JSONDecodeError as e: logger.error(f"[MQTT] JSON 解析错误: {e}") logger.error(f"[MQTT] Raw message: {msg.payload}") + logger.error(traceback.format_exc()) except Exception as e: logger.error(f"[MQTT] 处理消息时出错: {e}") + logger.error(traceback.format_exc()) def _on_disconnect(self, client, userdata, rc, reasonCode=None, properties=None): if rc != 0: diff --git a/unilabos/app/web/client.py b/unilabos/app/web/client.py index 1957f5dd..da5d0696 100644 --- a/unilabos/app/web/client.py +++ b/unilabos/app/web/client.py @@ -9,6 +9,7 @@ from typing import List, Dict, Any, Optional import requests from unilabos.utils.log import info from unilabos.config.config import MQConfig, HTTPConfig +from unilabos.utils import logger class HTTPClient: @@ -102,6 +103,30 @@ class HTTPClient: ) return response + def upload_file(self, file_path: str, scene: str = "models") -> requests.Response: + """ + 上传文件到服务器 + + 使用multipart/form-data格式上传文件,类似curl -F "files=@filepath" + + Args: + file_path: 要上传的文件路径 + scene: 上传场景,可选值为"user"或"models",默认为"models" + + Returns: + Response: API响应对象 + """ + with open(file_path, "rb") as file: + files = {"files": file} + logger.info(f"上传文件: {file_path} 到 {scene}") + response = requests.post( + f"{self.remote_addr}/api/account/file_upload/{scene}", + files=files, + headers={"Authorization": f"lab {self.auth}"}, + timeout=30, # 上传文件可能需要更长的超时时间 + ) + return response + # 创建默认客户端实例 http_client = HTTPClient() diff --git a/unilabos/app/web/utils/action_utils.py b/unilabos/app/web/utils/action_utils.py index 1af458f5..be2baa3f 100644 --- a/unilabos/app/web/utils/action_utils.py +++ b/unilabos/app/web/utils/action_utils.py @@ -8,7 +8,7 @@ import traceback from typing import Dict, Any, Type, TypedDict, Optional from rclpy.action import ActionClient, ActionServer -from rosidl_parser.definition import UnboundedSequence, NamespacedType, BasicType +from rosidl_parser.definition import UnboundedSequence, NamespacedType, BasicType, UnboundedString from unilabos.ros.msgs.message_converter import msg_converter_manager from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode @@ -74,7 +74,6 @@ def get_yaml_from_goal_type(goal_type) -> str: for ind, slot_info in enumerate(goal_type._fields_and_field_types.items()): slot_name, slot_type = slot_info type_info = goal_type.SLOT_TYPES[ind] - default_value = "unknown" if isinstance(type_info, UnboundedSequence): inner_type = type_info.value_type if isinstance(inner_type, NamespacedType): @@ -83,8 +82,10 @@ def get_yaml_from_goal_type(goal_type) -> str: default_value = [get_ros_msg_instance_as_dict(type_class())] elif isinstance(inner_type, BasicType): default_value = [get_default_value_for_ros_type(inner_type.typename)] + elif isinstance(inner_type, UnboundedString): + default_value = [""] else: - default_value = "unknown" + default_value = [] elif isinstance(type_info, NamespacedType): cls_name = ".".join(type_info.namespaces) + ":" + type_info.name type_class = msg_converter_manager.get_class(cls_name) @@ -93,6 +94,8 @@ def get_yaml_from_goal_type(goal_type) -> str: default_value = get_ros_msg_instance_as_dict(type_class()) elif isinstance(type_info, BasicType): default_value = get_default_value_for_ros_type(type_info.typename) + elif isinstance(type_info, UnboundedString): + default_value = "" else: type_class = msg_converter_manager.search_class(slot_type, search_lower=True) if type_class is not None: diff --git a/unilabos/config/config.py b/unilabos/config/config.py index 12ed4f6e..0cf999e6 100644 --- a/unilabos/config/config.py +++ b/unilabos/config/config.py @@ -13,6 +13,7 @@ class BasicConfig: is_host_mode = True # 从registry.py移动过来 slave_no_host = False # 是否跳过rclient.wait_for_service() machine_name = "undefined" + vis_2d_enable = False # MQTT配置 diff --git a/unilabos/device_mesh/__init__.py b/unilabos/device_mesh/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/unilabos/device_mesh/devices/opentrons_liquid_handler/joint_config.json b/unilabos/device_mesh/devices/opentrons_liquid_handler/joint_config.json new file mode 100644 index 00000000..e40339da --- /dev/null +++ b/unilabos/device_mesh/devices/opentrons_liquid_handler/joint_config.json @@ -0,0 +1,18 @@ +{ + "first_joint": { + "child":"first_link", + "axis" : "-y" + }, + "second_joint": { + "child":"second_link", + "axis" : "-x" + }, + "third_joint": { + "child":"third_link", + "axis" : "-z" + }, + "fourth_joint": { + "child":"fourth_link", + "axis" : "-z" + } +} \ No newline at end of file diff --git a/unilabos/device_mesh/devices/opentrons_liquid_handler/macro_device.xacro b/unilabos/device_mesh/devices/opentrons_liquid_handler/macro_device.xacro new file mode 100644 index 00000000..4e660557 --- /dev/null +++ b/unilabos/device_mesh/devices/opentrons_liquid_handler/macro_device.xacro @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-0.fbx b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-0.fbx new file mode 100644 index 00000000..5687a596 Binary files /dev/null and b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-0.fbx differ diff --git a/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-0.stl b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-0.stl new file mode 100644 index 00000000..5a2f71e8 Binary files /dev/null and b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-0.stl differ diff --git a/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-1.fbx b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-1.fbx new file mode 100644 index 00000000..201167d1 Binary files /dev/null and b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-1.fbx differ diff --git a/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-1.stl b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-1.stl new file mode 100644 index 00000000..a47b9020 Binary files /dev/null and b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-1.stl differ diff --git a/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-2.fbx b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-2.fbx new file mode 100644 index 00000000..4e716336 Binary files /dev/null and b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-2.fbx differ diff --git a/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-2.stl b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-2.stl new file mode 100644 index 00000000..a1d118ff Binary files /dev/null and b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-2.stl differ diff --git a/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-3a.fbx b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-3a.fbx new file mode 100644 index 00000000..32234365 Binary files /dev/null and b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-3a.fbx differ diff --git a/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-3a.stl b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-3a.stl new file mode 100644 index 00000000..7d31a8d6 Binary files /dev/null and b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-3a.stl differ diff --git a/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-3b.fbx b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-3b.fbx new file mode 100644 index 00000000..8f5fd4ff Binary files /dev/null and b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-3b.fbx differ diff --git a/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-3b.stl b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-3b.stl new file mode 100644 index 00000000..fa674839 Binary files /dev/null and b/unilabos/device_mesh/devices/opentrons_liquid_handler/meshes/ot2-3b.stl differ diff --git a/unilabos/device_mesh/devices/opentrons_liquid_handler/param_config.json b/unilabos/device_mesh/devices/opentrons_liquid_handler/param_config.json new file mode 100644 index 00000000..749feb47 --- /dev/null +++ b/unilabos/device_mesh/devices/opentrons_liquid_handler/param_config.json @@ -0,0 +1,10 @@ +{ + "private_param": + { + + }, + "public_param": + { + + } +} diff --git a/unilabos/device_mesh/devices/slide_w140/joint_config.json b/unilabos/device_mesh/devices/slide_w140/joint_config.json new file mode 100644 index 00000000..168bb0d4 --- /dev/null +++ b/unilabos/device_mesh/devices/slide_w140/joint_config.json @@ -0,0 +1,6 @@ +{ + "slider_joint": { + "child":"slider", + "axis" : "x" + } +} \ No newline at end of file diff --git a/unilabos/device_mesh/devices/slide_w140/macro_device.xacro b/unilabos/device_mesh/devices/slide_w140/macro_device.xacro new file mode 100644 index 00000000..7e43242e --- /dev/null +++ b/unilabos/device_mesh/devices/slide_w140/macro_device.xacro @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/unilabos/device_mesh/devices/slide_w140/meshes/base_link.STL b/unilabos/device_mesh/devices/slide_w140/meshes/base_link.STL new file mode 100755 index 00000000..f2ae347c Binary files /dev/null and b/unilabos/device_mesh/devices/slide_w140/meshes/base_link.STL differ diff --git a/unilabos/device_mesh/devices/slide_w140/meshes/base_link.fbx b/unilabos/device_mesh/devices/slide_w140/meshes/base_link.fbx new file mode 100644 index 00000000..8439d485 Binary files /dev/null and b/unilabos/device_mesh/devices/slide_w140/meshes/base_link.fbx differ diff --git a/unilabos/device_mesh/devices/slide_w140/meshes/length.STL b/unilabos/device_mesh/devices/slide_w140/meshes/length.STL new file mode 100755 index 00000000..df420447 Binary files /dev/null and b/unilabos/device_mesh/devices/slide_w140/meshes/length.STL differ diff --git a/unilabos/device_mesh/devices/slide_w140/meshes/length.fbx b/unilabos/device_mesh/devices/slide_w140/meshes/length.fbx new file mode 100644 index 00000000..b46c2c1e Binary files /dev/null and b/unilabos/device_mesh/devices/slide_w140/meshes/length.fbx differ diff --git a/unilabos/device_mesh/devices/slide_w140/meshes/slide_end.STL b/unilabos/device_mesh/devices/slide_w140/meshes/slide_end.STL new file mode 100755 index 00000000..3eaf9e01 Binary files /dev/null and b/unilabos/device_mesh/devices/slide_w140/meshes/slide_end.STL differ diff --git a/unilabos/device_mesh/devices/slide_w140/meshes/slide_end.fbx b/unilabos/device_mesh/devices/slide_w140/meshes/slide_end.fbx new file mode 100644 index 00000000..de0331ba Binary files /dev/null and b/unilabos/device_mesh/devices/slide_w140/meshes/slide_end.fbx differ diff --git a/unilabos/device_mesh/devices/slide_w140/meshes/slider.STL b/unilabos/device_mesh/devices/slide_w140/meshes/slider.STL new file mode 100755 index 00000000..d4b74347 Binary files /dev/null and b/unilabos/device_mesh/devices/slide_w140/meshes/slider.STL differ diff --git a/unilabos/device_mesh/devices/slide_w140/meshes/slider.fbx b/unilabos/device_mesh/devices/slide_w140/meshes/slider.fbx new file mode 100644 index 00000000..38b9814b Binary files /dev/null and b/unilabos/device_mesh/devices/slide_w140/meshes/slider.fbx differ diff --git a/unilabos/device_mesh/devices/slide_w140/param_config.json b/unilabos/device_mesh/devices/slide_w140/param_config.json new file mode 100644 index 00000000..d1c9213b --- /dev/null +++ b/unilabos/device_mesh/devices/slide_w140/param_config.json @@ -0,0 +1,12 @@ +{ + "private_param": + { + "min_d": 0.1 , + "max_d": 0.1 , + "slider_d": 0.14 + }, + "public_param": + { + "length" :0.1 + } +} \ No newline at end of file diff --git a/unilabos/device_mesh/resource_visalization.py b/unilabos/device_mesh/resource_visalization.py new file mode 100644 index 00000000..b840789f --- /dev/null +++ b/unilabos/device_mesh/resource_visalization.py @@ -0,0 +1,174 @@ +import os +from pathlib import Path +from launch import LaunchService +from launch import LaunchDescription +from launch_ros.actions import Node as nd +import xacro +from lxml import etree + +from unilabos.registry.registry import lab_registry + + +class ResourceVisualization: + def __init__(self, device: dict, resource: dict, enable_rviz: bool = True): + """初始化资源可视化类 + + 该类用于将设备和资源的3D模型可视化展示。通过解析设备和资源的配置信息, + 从注册表中获取对应的3D模型文件,并使用ROS2和RViz进行可视化。 + + Args: + device (dict): 设备配置字典,包含设备的类型、位置等信息 + resource (dict): 资源配置字典,包含资源的类型、位置等信息 + registry (dict): 注册表字典,包含设备和资源类型的注册信息 + enable_rviz (bool, optional): 是否启用RViz可视化. Defaults to True. + """ + self.launch_service = LaunchService() + self.launch_description = LaunchDescription() + self.resource_dict = resource + self.resource_model = {} + self.resource_type = ['deck', 'plate', 'container'] + self.mesh_path = Path(__file__).parent.absolute() + self.enable_rviz = enable_rviz + registry = lab_registry + + self.srdf_str = ''' + + + + + ''' + self.robot_state_str= ''' + + + + ''' + self.root = etree.fromstring(self.robot_state_str) + + xacro_uri = self.root.nsmap["xacro"] + + # 遍历设备节点 + for node in device.values(): + if node['type'] == 'device' and node['class'] != '': + device_class = node['class'] + # 检查设备类型是否在注册表中 + if device_class not in registry.device_type_registry.keys(): + raise ValueError(f"设备类型 {device_class} 未在注册表中注册") + elif node['type'] in self.resource_type: + # print(registry.resource_type_registry) + resource_class = node['class'] + if resource_class not in registry.resource_type_registry.keys(): + raise ValueError(f"资源类型 {resource_class} 未在注册表中注册") + elif "model" in registry.resource_type_registry[resource_class].keys(): + model_config = registry.resource_type_registry[resource_class]['model'] + if model_config['type'] == 'resource': + self.resource_model[node['id']] = { + 'mesh': f"{str(self.mesh_path)}/resources/{model_config['mesh']}", + 'mesh_tf': model_config['mesh_tf']} + if 'children_mesh' in model_config: + if model_config['children_mesh'] is not None: + self.resource_model[f"{node['id']}_"] = { + 'mesh': f"{str(self.mesh_path)}/resources/{model_config['children_mesh']}", + 'mesh_tf': model_config['children_mesh_tf'] + } + elif model_config['type'] == 'device': + new_include = etree.SubElement(self.root, f"{{{xacro_uri}}}include") + new_include.set("filename", f"{str(self.mesh_path)}/devices/{model_config['mesh']}/macro_device.xacro") + new_dev = etree.SubElement(self.root, f"{{{xacro_uri}}}{model_config['mesh']}") + new_dev.set("parent_link", "world") + new_dev.set("mesh_path", str(self.mesh_path)) + new_dev.set("device_name", node["id"]+"_") + new_dev.set("station_name", node["parent"]+'_') + new_dev.set("x",str(float(node["position"]["x"])/1000)) + new_dev.set("y",str(float(node["position"]["y"])/1000)) + new_dev.set("z",str(float(node["position"]["z"])/1000)) + if "rotation" in node["config"]: + new_dev.set("r",str(float(node["config"]["rotation"]["z"])/1000)) + else: + print("错误的注册表类型!") + re = etree.tostring(self.root, encoding="unicode") + doc = xacro.parse(re) + xacro.process_doc(doc) + self.urdf_str = doc.toxml() + + + def create_launch_description(self, urdf_str: str) -> LaunchDescription: + """ + 创建launch描述,包含robot_state_publisher和move_group节点 + + Args: + urdf_str: URDF文本 + + Returns: + LaunchDescription: launch描述对象 + """ + + + # 解析URDF文件 + robot_description = urdf_str + + # 创建robot_state_publisher节点 + robot_state_publisher = nd( + package='robot_state_publisher', + executable='robot_state_publisher', + name='robot_state_publisher', + output='screen', + parameters=[{ + 'robot_description': robot_description, + 'use_sim_time': False + }] + ) + + # joint_state_publisher_node = nd( + # package='joint_state_publisher_gui', # 或 joint_state_publisher + # executable='joint_state_publisher_gui', + # name='joint_state_publisher', + # output='screen' + # ) + # 创建move_group节点 + move_group = nd( + package='moveit_ros_move_group', + executable='move_group', + output='screen', + parameters=[{ + 'robot_description': robot_description, + 'robot_description_semantic': self.srdf_str, + 'capabilities': '', + 'disable_capabilities': '', + 'monitor_dynamics': False, + 'publish_monitored_planning_scene': True, + 'publish_robot_description_semantic': True, + 'publish_planning_scene': True, + 'publish_geometry_updates': True, + 'publish_state_updates': True, + 'publish_transforms_updates': True, + }] + ) + + # 将节点添加到launch描述中 + self.launch_description.add_action(robot_state_publisher) + # self.launch_description.add_action(joint_state_publisher_node) + self.launch_description.add_action(move_group) + + # 如果启用RViz,添加RViz节点 + if self.enable_rviz: + rviz_node = nd( + package='rviz2', + executable='rviz2', + name='rviz2', + arguments=['-d', f"{str(self.mesh_path)}/view_robot.rviz"], + output='screen' + ) + self.launch_description.add_action(rviz_node) + + return self.launch_description + + def start(self) -> None: + """ + 启动可视化服务 + + Args: + urdf_str: URDF文件路径 + """ + launch_description = self.create_launch_description(self.urdf_str) + self.launch_service.include_launch_description(launch_description) + self.launch_service.run() \ No newline at end of file diff --git a/unilabos/device_mesh/resources/generic_labware_tube_10_75/0_base.png b/unilabos/device_mesh/resources/generic_labware_tube_10_75/0_base.png new file mode 100644 index 00000000..d9de252f Binary files /dev/null and b/unilabos/device_mesh/resources/generic_labware_tube_10_75/0_base.png differ diff --git a/unilabos/device_mesh/resources/generic_labware_tube_10_75/meshes/0_base.glb b/unilabos/device_mesh/resources/generic_labware_tube_10_75/meshes/0_base.glb new file mode 100644 index 00000000..1c64cc4e Binary files /dev/null and b/unilabos/device_mesh/resources/generic_labware_tube_10_75/meshes/0_base.glb differ diff --git a/unilabos/device_mesh/resources/generic_labware_tube_10_75/meshes/0_base.stl b/unilabos/device_mesh/resources/generic_labware_tube_10_75/meshes/0_base.stl new file mode 100644 index 00000000..023cf1a2 Binary files /dev/null and b/unilabos/device_mesh/resources/generic_labware_tube_10_75/meshes/0_base.stl differ diff --git a/unilabos/device_mesh/resources/generic_labware_tube_10_75/meta.json b/unilabos/device_mesh/resources/generic_labware_tube_10_75/meta.json new file mode 100644 index 00000000..84f4657b --- /dev/null +++ b/unilabos/device_mesh/resources/generic_labware_tube_10_75/meta.json @@ -0,0 +1,22 @@ +{ + "fileName": "generic_labware_tube_10_75", + "related": [ + "generic_labware_0.5ml_screw_cap_tube", + "generic_labware_0.5ml_tube_rack", + "generic_labware_12_well_plate", + "sarstedt_14x200mm_tube", + "sarstedt_18x200mm_tube", + "generic_labware_1ml_tube_rack", + "generic_labware_24_well_plate", + "generic_labware_2ml_screw_cap_tube", + "generic_labware_5ml_screw_cap_tube", + "generic_labware_6_well_plate", + "generic_labware_96_well_square", + "generic_labware_96_well_pcr_plate_round", + "generic_labware_framedtiprack", + "generic_labware_plate_lid", + "generic_labware_reservoir", + "generic_labware_tip_box", + "generic_labware_tube_10_75" + ] +} diff --git a/unilabos/device_mesh/resources/generic_labware_tube_10_75/modal.xacro b/unilabos/device_mesh/resources/generic_labware_tube_10_75/modal.xacro new file mode 100644 index 00000000..65711112 --- /dev/null +++ b/unilabos/device_mesh/resources/generic_labware_tube_10_75/modal.xacro @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/unilabos/device_mesh/resources/tecan_nested_tip_rack/meshes/plate.glb b/unilabos/device_mesh/resources/tecan_nested_tip_rack/meshes/plate.glb new file mode 100644 index 00000000..afb6c99b Binary files /dev/null and b/unilabos/device_mesh/resources/tecan_nested_tip_rack/meshes/plate.glb differ diff --git a/unilabos/device_mesh/resources/tecan_nested_tip_rack/meshes/plate.stl b/unilabos/device_mesh/resources/tecan_nested_tip_rack/meshes/plate.stl new file mode 100644 index 00000000..2655ff94 Binary files /dev/null and b/unilabos/device_mesh/resources/tecan_nested_tip_rack/meshes/plate.stl differ diff --git a/unilabos/device_mesh/resources/tecan_nested_tip_rack/meta.json b/unilabos/device_mesh/resources/tecan_nested_tip_rack/meta.json new file mode 100644 index 00000000..1b437d1f --- /dev/null +++ b/unilabos/device_mesh/resources/tecan_nested_tip_rack/meta.json @@ -0,0 +1,86 @@ +{ + "fileName": "tecan_nested_tip_rack", + "related": [ + "tecan_techrom", + "tecan_holder_transfer_tool", + "tecan_fluent_9_grid_segment_cutout", + "tecan_fluent_centric_gripper", + "tecan_fluent_eccentric_gripper", + "tecan_evo100", + "tecan_fluent_mp_diti_nest_segment", + "tecan_fluent_4x100_trough", + "tecan_fluent_1080_extended", + "tecan_fluent_1_1_1000_trough", + "tecan_fluent_50ml_tube_runner_10_v2", + "tecan_fluent_15ml_tube_runner_16_v2", + "tecan_fluent_1_16_16_tube_runner", + "tecan_fluent_1.5ml_tube_runner_v2", + "tecan_fluent_1_24_10_tube_runner", + "tecan_fluent_1_24_13_tube_runner", + "tecan_fluent_3x320_reagent_trough_v2", + "tecan_fluent_32_tube_runner_v2", + "tecan_fluent_1_4_100_trough", + "tecan_fluent_2_grid_segment", + "tecan_fluent_2_4_100_trough_waste", + "tecan_fluent_3_grid_segment", + "tecan_fluent_nest_waste_segment_v2", + "tecan_fluent_320ml_reagent_trough", + "tecan_fluent_4_landscape_61mm_nest_segment", + "tecan_fluent_4_landscape_61mm_nest_segment_waste", + "tecan_fluent_4_landscape_7mm_nest_segment", + "tecan_fluent_4_landscape_7mm_nest_segment_waste", + "tecan_fluent_hotel_deck_4", + "tecan_fluent_480_extended", + "tecan_fluent_4x100_reagent_trough_v2", + "tecan_fluent_5_landscape_61mm_nest_segment", + "tecan_fluent_5_landscape_7mm_nest_segment", + "tecan_fluent_hotel_deck_5", + "tecan_fluent_6_grid_segment", + "tecan_fluent_nest_landscape_segment_v2", + "tecan_fluent_6_landscape_7mm_nest_segment", + "tecan_fluent_deck_segment_6_v2", + "tecan_fluent_fca_diti_segment_v2", + "tecan_fluent_6_nest_incubator", + "tecan_fluent_plate_nest", + "tecan_fluent_780_extended", + "tecan_fluent_plate_holder", + "tecan_fluent_8_grid_segment", + "tecan_fluent_8_grid_segment_evo", + "tecan_fluent_hotel_deck_9", + "tecan_carousel", + "tecan_carousel_stacker_10", + "tecan_carousel_stacker_25", + "tecan_carousel_stacker_6", + "tecan_fluent_coolheat_microplate_segment_v2", + "tecan_fluent_fca_diti_tray", + "tecan_fluent_trough_waste", + "tecan_fluent_id_left", + "tecan_fluent_id_middle", + "tecan_fluent_lower_6_grid_v2", + "tecan_fluent_mc384_nest", + "tecan_fluent_mca_44mm_nest", + "tecan_fluent_deck_segment_4_v2", + "tecan_fluent_mca_base_segment_384_v2", + "tecan_fluent_waste_module", + "tecan_fluent_reagent_block", + "tecan_fluent_tube_grippers", + "tecan_fluent_washstation_waste_v2", + "tecan_carrier_additive_trough_3_pce_max_100ml", + "tecan_carrier_384_well_mp_3_pos_accessible_roma", + "tecan_carrier_rack_3_diti_width_6", + "tecan_transport_box_diti_tray_1000ul", + "tecan_transport_box_diti_tray_200ul", + "tecan_magicprep_ngs_sample_deck", + "tecan_fluent_shelf_large", + "tecan_fluent_shelf_small", + "tecan_spacer_29_9_te_chrom", + "tecan_teshake_adapter_2", + "tecan_teshake_base", + "tecan_tevacs_base", + "tecan_tevacs_plate_park", + "tecan_tevacs_spacer", + "tecan_tevacs_vacuum", + "tecan_tip_box", + "tecan_nested_tip_rack" + ] +} diff --git a/unilabos/device_mesh/resources/tecan_nested_tip_rack/modal.xacro b/unilabos/device_mesh/resources/tecan_nested_tip_rack/modal.xacro new file mode 100644 index 00000000..e29617fa --- /dev/null +++ b/unilabos/device_mesh/resources/tecan_nested_tip_rack/modal.xacro @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/unilabos/device_mesh/resources/tecan_nested_tip_rack/plate.png b/unilabos/device_mesh/resources/tecan_nested_tip_rack/plate.png new file mode 100644 index 00000000..34d7ac68 Binary files /dev/null and b/unilabos/device_mesh/resources/tecan_nested_tip_rack/plate.png differ diff --git a/unilabos/device_mesh/view_robot.rviz b/unilabos/device_mesh/view_robot.rviz new file mode 100644 index 00000000..64f6b358 --- /dev/null +++ b/unilabos/device_mesh/view_robot.rviz @@ -0,0 +1,387 @@ +Panels: + - Class: rviz_common/Displays + Help Height: 138 + Name: Displays + Property Tree Widget: + Expanded: + - /Global Options1 + - /TF1 + - /TF1/Tree1 + - /RobotModel1 + - /PlanningScene1 + - /PlanningScene1/Scene Geometry1 + - /RobotState1 + - /RobotState1/Links1 + - /MotionPlanning1 + - /MotionPlanning1/Scene Geometry1 + - /MotionPlanning1/Scene Robot1 + - /MotionPlanning1/Planning Request1 + Splitter Ratio: 0.5 + Tree Height: 345 + - Class: rviz_common/Selection + Name: Selection + - Class: rviz_common/Tool Properties + Expanded: + - /2D Goal Pose1 + - /Publish Point1 + Name: Tool Properties + Splitter Ratio: 0.5886790156364441 + - Class: rviz_common/Views + Expanded: + - /Current View1 + Name: Views + Splitter Ratio: 0.5 +Visualization Manager: + Class: "" + Displays: + - Alpha: 0.5 + Cell Size: 1 + Class: rviz_default_plugins/Grid + Color: 160; 160; 164 + Enabled: true + Line Style: + Line Width: 0.029999999329447746 + Value: Lines + Name: Grid + Normal Cell Count: 0 + Offset: + X: 0 + Y: 0 + Z: 0 + Plane: XY + Plane Cell Count: 10 + Reference Frame: + Value: true + - Class: rviz_default_plugins/TF + Enabled: false + Frame Timeout: 15 + Frames: + All Enabled: false + Marker Scale: 1 + Name: TF + Show Arrows: true + Show Axes: true + Show Names: false + Tree: + {} + Update Interval: 0 + Value: false + - Alpha: 1 + Class: rviz_default_plugins/RobotModel + Collision Enabled: false + Description File: "" + Description Source: Topic + Description Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /robot_description + Enabled: false + Links: + All Links Enabled: true + Expand Joint Details: false + Expand Link Details: false + Expand Tree: false + Link Tree Style: Links in Alphabetic Order + Mass Properties: + Inertia: false + Mass: false + Name: RobotModel + TF Prefix: "" + Update Interval: 0 + Value: false + Visual Enabled: true + - Class: moveit_rviz_plugin/PlanningScene + Enabled: false + Move Group Namespace: "" + Name: PlanningScene + Planning Scene Topic: /monitored_planning_scene + Robot Description: robot_description + Scene Geometry: + Scene Alpha: 0.8999999761581421 + Scene Color: 50; 230; 50 + Scene Display Time: 0.009999999776482582 + Show Scene Geometry: true + Voxel Coloring: Z-Axis + Voxel Rendering: Occupied Voxels + Scene Robot: + Attached Body Color: 150; 50; 150 + Links: + All Links Enabled: true + Expand Joint Details: false + Expand Link Details: false + Expand Tree: false + Link Tree Style: Links in Alphabetic Order + Robot Alpha: 1 + Show Robot Collision: false + Show Robot Visual: false + Value: false + - Attached Body Color: 150; 50; 150 + Class: moveit_rviz_plugin/RobotState + Collision Enabled: false + Enabled: false + Links: + All Links Enabled: true + Expand Joint Details: false + Expand Link Details: false + Expand Tree: false + Link Tree Style: Links in Alphabetic Order + Name: RobotState + Robot Alpha: 1 + Robot Description: robot_description + Robot State Topic: display_robot_state + Show All Links: true + Show Highlights: true + Value: false + Visual Enabled: true + - Acceleration_Scaling_Factor: 0.1 + Class: moveit_rviz_plugin/MotionPlanning + Enabled: true + Move Group Namespace: "" + MoveIt_Allow_Approximate_IK: false + MoveIt_Allow_External_Program: false + MoveIt_Allow_Replanning: false + MoveIt_Allow_Sensor_Positioning: false + MoveIt_Planning_Attempts: 10 + MoveIt_Planning_Time: 5 + MoveIt_Use_Cartesian_Path: false + MoveIt_Use_Constraint_Aware_IK: false + MoveIt_Workspace: + Center: + X: 0 + Y: 0 + Z: 0 + Size: + X: 2 + Y: 2 + Z: 2 + Name: MotionPlanning + Planned Path: + Color Enabled: false + Interrupt Display: false + Links: + All Links Enabled: true + Expand Joint Details: false + Expand Link Details: false + Expand Tree: false + Link Tree Style: Links in Alphabetic Order + PLR_STATION_deck_device_link: + Alpha: 1 + Show Axes: false + Show Trail: false + PLR_STATION_deck_first_link: + Alpha: 1 + Show Axes: false + Show Trail: false + Value: true + PLR_STATION_deck_fourth_link: + Alpha: 1 + Show Axes: false + Show Trail: false + Value: true + PLR_STATION_deck_main_link: + Alpha: 1 + Show Axes: false + Show Trail: false + Value: true + PLR_STATION_deck_second_link: + Alpha: 1 + Show Axes: false + Show Trail: false + Value: true + PLR_STATION_deck_socketTypeGenericSbsFootprint: + Alpha: 1 + Show Axes: false + Show Trail: false + PLR_STATION_deck_socketTypeHEPAModule: + Alpha: 1 + Show Axes: false + Show Trail: false + PLR_STATION_deck_third_link: + Alpha: 1 + Show Axes: false + Show Trail: false + Value: true + world: + Alpha: 1 + Show Axes: false + Show Trail: false + Loop Animation: false + Robot Alpha: 0.5 + Robot Color: 150; 50; 150 + Show Robot Collision: false + Show Robot Visual: true + Show Trail: false + State Display Time: 3x + Trail Step Size: 1 + Trajectory Topic: /display_planned_path + Use Sim Time: false + Planning Metrics: + Payload: 1 + Show Joint Torques: false + Show Manipulability: false + Show Manipulability Index: false + Show Weight Limit: false + TextHeight: 0.07999999821186066 + Planning Request: + Colliding Link Color: 255; 0; 0 + Goal State Alpha: 1 + Goal State Color: 250; 128; 0 + Interactive Marker Size: 0 + Joint Violation Color: 255; 0; 255 + Planning Group: "" + Query Goal State: false + Query Start State: false + Show Workspace: false + Start State Alpha: 1 + Start State Color: 0; 255; 0 + Planning Scene Topic: /monitored_planning_scene + Robot Description: robot_description + Scene Geometry: + Scene Alpha: 0.8999999761581421 + Scene Color: 50; 230; 50 + Scene Display Time: 0.009999999776482582 + Show Scene Geometry: true + Voxel Coloring: Z-Axis + Voxel Rendering: Occupied Voxels + Scene Robot: + Attached Body Color: 150; 50; 150 + Links: + All Links Enabled: true + Expand Joint Details: false + Expand Link Details: false + Expand Tree: false + Link Tree Style: Links in Alphabetic Order + PLR_STATION_deck_device_link: + Alpha: 1 + Show Axes: false + Show Trail: false + PLR_STATION_deck_first_link: + Alpha: 1 + Show Axes: false + Show Trail: false + Value: true + PLR_STATION_deck_fourth_link: + Alpha: 1 + Show Axes: false + Show Trail: false + Value: true + PLR_STATION_deck_main_link: + Alpha: 1 + Show Axes: false + Show Trail: false + Value: true + PLR_STATION_deck_second_link: + Alpha: 1 + Show Axes: false + Show Trail: false + Value: true + PLR_STATION_deck_socketTypeGenericSbsFootprint: + Alpha: 1 + Show Axes: false + Show Trail: false + PLR_STATION_deck_socketTypeHEPAModule: + Alpha: 1 + Show Axes: false + Show Trail: false + PLR_STATION_deck_third_link: + Alpha: 1 + Show Axes: false + Show Trail: false + Value: true + world: + Alpha: 1 + Show Axes: false + Show Trail: false + Robot Alpha: 1 + Show Robot Collision: false + Show Robot Visual: true + Value: true + Velocity_Scaling_Factor: 0.1 + Enabled: true + Global Options: + Background Color: 48; 48; 48 + Fixed Frame: world + Frame Rate: 30 + Name: root + Tools: + - Class: rviz_default_plugins/Interact + Hide Inactive Objects: true + - Class: rviz_default_plugins/MoveCamera + - Class: rviz_default_plugins/Select + - Class: rviz_default_plugins/FocusCamera + - Class: rviz_default_plugins/Measure + Line color: 128; 128; 0 + - Class: rviz_default_plugins/SetInitialPose + Covariance x: 0.25 + Covariance y: 0.25 + Covariance yaw: 0.06853891909122467 + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /initialpose + - Class: rviz_default_plugins/SetGoal + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /goal_pose + - Class: rviz_default_plugins/PublishPoint + Single click: true + Topic: + Depth: 5 + Durability Policy: Volatile + History Policy: Keep Last + Reliability Policy: Reliable + Value: /clicked_point + Transformation: + Current: + Class: rviz_default_plugins/TF + Value: true + Views: + Current: + Class: rviz_default_plugins/Orbit + Distance: 1.0284695625305176 + Enable Stereo Rendering: + Stereo Eye Separation: 0.05999999865889549 + Stereo Focal Distance: 1 + Swap Stereo Eyes: false + Value: false + Focal Point: + X: 0.29730814695358276 + Y: 0.21228469908237457 + Z: 0.20008830726146698 + Focal Shape Fixed Size: true + Focal Shape Size: 0.05000000074505806 + Invert Z Axis: false + Name: Current View + Near Clip Distance: 0.009999999776482582 + Pitch: 0.38979560136795044 + Target Frame: + Value: Orbit (rviz) + Yaw: 0.06074193865060806 + Saved: ~ +Window Geometry: + Displays: + collapsed: false + Height: 1656 + Hide Left Dock: false + Hide Right Dock: true + MotionPlanning: + collapsed: false + MotionPlanning - Trajectory Slider: + collapsed: false + QMainWindow State: 000000ff00000000fd0000000400000000000003a3000005dcfc020000000bfb0000001200530065006c0065006300740069006f006e00000001e10000009b000000b000fffffffb0000001e0054006f006f006c002000500072006f007000650072007400690065007302000001ed000001df00000185000000a3fb000000120056006900650077007300200054006f006f02000001df000002110000018500000122fb000000200054006f006f006c002000500072006f0070006500720074006900650073003203000002880000011d000002210000017afb000000100044006900730070006c006100790073010000006e000002510000018200fffffffb0000002000730065006c0065006300740069006f006e00200062007500660066006500720200000138000000aa0000023a00000294fb00000014005700690064006500530074006500720065006f02000000e6000000d2000003ee0000030bfb0000000c004b0069006e0065006300740200000186000001060000030c00000261fb000000280020002d0020005400720061006a006500630074006f0072007900200053006c00690064006500720000000000ffffffff0000000000000000fb00000044004d006f00740069006f006e0050006c0061006e006e0069006e00670020002d0020005400720061006a006500630074006f0072007900200053006c00690064006500720000000000ffffffff0000007a00fffffffb0000001c004d006f00740069006f006e0050006c0061006e006e0069006e006701000002cb0000037f000002b800ffffff000000010000010f00000387fc0200000003fb0000001e0054006f006f006c002000500072006f00700065007200740069006500730100000041000000780000000000000000fb0000000a00560069006500770073000000003b000003870000013200fffffffb0000001200530065006c0065006300740069006f006e010000025a000000b200000000000000000000000200000490000000a9fc0100000001fb0000000a00560069006500770073030000004e00000080000002e10000019700000003000004420000003efc0100000002fb0000000800540069006d00650100000000000004420000000000000000fb0000000800540069006d0065010000000000000450000000000000000000000627000005dc00000004000000040000000800000008fc0000000100000002000000010000000a0054006f006f006c00730100000000ffffffff0000000000000000 + Selection: + collapsed: false + Tool Properties: + collapsed: false + Views: + collapsed: true + Width: 2518 + X: 385 + Y: 120 diff --git a/unilabos/devices/agv/ur_arm_task.py b/unilabos/devices/agv/ur_arm_task.py index 8c84c855..47a7c931 100644 --- a/unilabos/devices/agv/ur_arm_task.py +++ b/unilabos/devices/agv/ur_arm_task.py @@ -77,9 +77,6 @@ class UrArmTask(): if n > retry: raise Exception('Can not connect to the arm info server!') - self.pose_data = {} - self.pose_file = 'C:\\auto\\unilabos\\unilabos\\devices\\agv\\pose.json' - self.reload_pose() self.dash_c.stop() def arm_init(self): diff --git a/unilabos/devices/liquid_handling/action_definition.py b/unilabos/devices/liquid_handling/action_definition.py index 9cefa1ce..530703ad 100644 --- a/unilabos/devices/liquid_handling/action_definition.py +++ b/unilabos/devices/liquid_handling/action_definition.py @@ -36,6 +36,7 @@ class DPLiquidHandler(LiquidHandler): delays: Optional[List[int]] = None, is_96_well: Optional[bool] = False, top: Optional[List(float)] = None, + none_keys: List[str] = [] ): """A complete *remove* (aspirate → waste) operation.""" trash = self.deck.get_trash_area() @@ -98,7 +99,8 @@ class DPLiquidHandler(LiquidHandler): mix_time: Optional[int] = None, mix_vol: Optional[int] = None, mix_rate: Optional[int] = None, - mix_liquid_height: Optional[float] = None + mix_liquid_height: Optional[float] = None, + none_keys: List[str] = [] ): """A complete *add* (aspirate reagent → dispense into targets) operation.""" @@ -177,6 +179,7 @@ class DPLiquidHandler(LiquidHandler): mix_rate: Optional[int] = None, mix_liquid_height: Optional[float] = None, delays: Optional[List[int]] = None, + none_keys: List[str] = [] ): """Transfer liquid from each *source* well/plate to the corresponding *target*. @@ -295,6 +298,7 @@ class DPLiquidHandler(LiquidHandler): height_to_bottom: Optional[float] = None, offsets: Optional[Coordinate] = None, mix_rate: Optional[float] = None, + none_keys: List[str] = [] ): if mix_time is None: # No mixing required return diff --git a/unilabos/devices/ros_dev/__init__.py b/unilabos/devices/ros_dev/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/unilabos/devices/ros_dev/liquid_handler_joint_publisher.py b/unilabos/devices/ros_dev/liquid_handler_joint_publisher.py new file mode 100644 index 00000000..e593f425 --- /dev/null +++ b/unilabos/devices/ros_dev/liquid_handler_joint_publisher.py @@ -0,0 +1,208 @@ +import copy +import rclpy +import json +import time +from rclpy.executors import MultiThreadedExecutor +from rclpy.action import ActionServer +from sensor_msgs.msg import JointState +from unilabos_msgs.action import SendCmd +from rclpy.action.server import ServerGoalHandle +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode +from tf_transformations import quaternion_from_euler +from tf2_ros import TransformBroadcaster, Buffer, TransformListener + + +class LiquidHandlerJointPublisher(BaseROS2DeviceNode): + def __init__(self,device_id:str, joint_config:dict, lh_id:str,resource_tracker, rate=50): + super().__init__( + driver_instance=self, + device_id=device_id, + status_types={}, + action_value_mappings={}, + hardware_interface={}, + print_publish=False, + resource_tracker=resource_tracker, + ) + + # joint_config_dict = { + # "joint_names":[ + # "first_joint", + # "second_joint", + # "third_joint", + # "fourth_joint" + # ], + # "y":{ + # "first_joint":{ + # "factor":-1, + # "offset":0.0 + # } + # }, + # "x":{ + # "second_joint":{ + # "factor":-1, + # "offset":0.0 + # } + # }, + # "z":{ + # "third_joint":{ + # "factor":1, + # "offset":0.0 + # }, + # "fourth_joint":{ + # "factor":1, + # "offset":0.0 + # } + # } + # } + + self.j_msg = JointState() + self.lh_id = lh_id + # self.j_msg.name = joint_names + self.joint_config = joint_config + self.j_msg.position = [0.0 for i in range(len(joint_config['joint_names']))] + self.j_msg.name = [f"{self.lh_id}_{x}" for x in joint_config['joint_names']] + # self.joint_config = joint_config_dict + # self.j_msg.position = [0.0 for i in range(len(joint_config_dict['joint_names']))] + # self.j_msg.name = [f"{self.lh_id}_{x}" for x in joint_config_dict['joint_names']] + self.rate = rate + self.tf_buffer = Buffer() + self.tf_listener = TransformListener(self.tf_buffer, self) + self.j_pub = self.create_publisher(JointState,'/joint_states',10) + self.create_timer(0.02,self.lh_joint_pub_callback) + self.j_action = ActionServer( + self, + SendCmd, + "joint", + self.lh_joint_action_callback, + result_timeout=5000 + ) + + def lh_joint_action_callback(self,goal_handle: ServerGoalHandle): + """Move a single joint + + Args: + command: A JSON-formatted string that includes joint_name, speed, position + + joint_name (str): The name of the joint to move + speed (float): The speed of the movement, speed > 0 + position (float): The position to move to + + Returns: + None + """ + result = SendCmd.Result() + cmd_str = str(goal_handle.request.command).replace('\'','\"') + # goal_handle.execute() + + try: + cmd_dict = json.loads(cmd_str) + self.move_joints(**cmd_dict) + result.success = True + goal_handle.succeed() + + except Exception as e: + print(e) + goal_handle.abort() + result.success = False + + return result + def inverse_kinematics(self, x, y, z, + x_joint:dict, + y_joint:dict, + z_joint:dict ): + """ + 将x、y、z坐标转换为对应关节的位置 + + Args: + x (float): x坐标 + y (float): y坐标 + z (float): z坐标 + x_joint (dict): x轴关节配置,包含plus和offset + y_joint (dict): y轴关节配置,包含plus和offset + z_joint (dict): z轴关节配置,包含plus和offset + + Returns: + dict: 关节名称和对应位置的字典 + """ + joint_positions = copy.deepcopy(self.j_msg.position) + + # 处理x轴关节 + for joint_name, config in x_joint.items(): + index = self.j_msg.name.index(f"{self.lh_id}_{joint_name}") + joint_positions[index] = x * config["factor"] + config["offset"] + + # 处理y轴关节 + for joint_name, config in y_joint.items(): + index = self.j_msg.name.index(f"{self.lh_id}_{joint_name}") + joint_positions[index] = y * config["factor"] + config["offset"] + + # 处理z轴关节 + for joint_name, config in z_joint.items(): + index = self.j_msg.name.index(f"{self.lh_id}_{joint_name}") + joint_positions[index] = z * config["factor"] + config["offset"] + + + return joint_positions + + + def move_joints(self, resource_name, link_name, speed, x_joint=None, y_joint=None, z_joint=None): + + transform = self.tf_buffer.lookup_transform( + link_name, + resource_name, + rclpy.time.Time() + ) + x,y,z = transform.transform.translation.x, transform.transform.translation.y, transform.transform.translation.z + if x_joint is None: + x_joint_config = next(iter(self.joint_config['x'].items())) + elif x_joint in self.joint_config['x']: + x_joint_config = self.joint_config['x'][x_joint] + else: + raise ValueError(f"x_joint {x_joint} not in joint_config['x']") + if y_joint is None: + y_joint_config = next(iter(self.joint_config['y'].items())) + elif y_joint in self.joint_config['y']: + y_joint_config = self.joint_config['y'][y_joint] + else: + raise ValueError(f"y_joint {y_joint} not in joint_config['y']") + if z_joint is None: + z_joint_config = next(iter(self.joint_config['z'].items())) + elif z_joint in self.joint_config['z']: + z_joint_config = self.joint_config['z'][z_joint] + else: + raise ValueError(f"z_joint {z_joint} not in joint_config['z']") + joint_positions_target = self.inverse_kinematics(x,y,z,x_joint_config,y_joint_config,z_joint_config) + + loop_flag = 0 + + + while loop_flag < len(self.joint_config['joint_names']): + loop_flag = 0 + for i in range(len(self.joint_config['joint_names'])): + distance = joint_positions_target[i] - self.j_msg.position[i] + if distance == 0: + loop_flag += 1 + continue + minus_flag = distance/abs(distance) + if abs(distance) > speed/self.rate: + self.j_msg.position[i] += minus_flag * speed/self.rate + else : + self.j_msg.position[i] = joint_positions_target[i] + loop_flag += 1 + + + # 发布关节状态 + self.lh_joint_pub_callback() + time.sleep(1/self.rate) + + + def lh_joint_pub_callback(self): + self.j_msg.header.stamp = self.get_clock().now().to_msg() + self.j_pub.publish(self.j_msg) + +def main(): + + pass + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/unilabos/registry/devices/liquid_handler.yaml b/unilabos/registry/devices/liquid_handler.yaml index 4451ca06..ba921c77 100644 --- a/unilabos/registry/devices/liquid_handler.yaml +++ b/unilabos/registry/devices/liquid_handler.yaml @@ -163,13 +163,134 @@ liquid_handler: schema: type: object properties: - status: + name: type: string description: 液体处理仪器当前状态 required: - - status + - name additionalProperties: false +dp_liquid_handler: + description: 通用液体处理 + class: + module: unilabos.devices.liquid_handling.action_definition:DPLiquidHandler + type: python + status_types: + status: String + action_value_mappings: + remove_liquid: + type: DPLiquidHandlerRemoveLiquid + goal: + vols: vols + sources: sources + waste_liquid: waste_liquid + use_channels: use_channels + flow_rates: flow_rates + offsets: offsets + liquid_height: liquid_height + blow_out_air_volume: blow_out_air_volume + spread: spread + delays: delays + is_96_well: is_96_well + top: top + none_keys: none_keys + feedback: {} + result: {} + add_liquid: + type: DPLiquidHandlerAddLiquid + goal: + asp_vols: asp_vols + dis_vols: dis_vols + reagent_sources: reagent_sources + targets: targets + use_channels: use_channels + flow_rates: flow_rates + offsets: offsets + liquid_height: liquid_height + blow_out_air_volume: blow_out_air_volume + spread: spread + is_96_well: is_96_well + mix_time: mix_time + mix_vol: mix_vol + mix_rate: mix_rate + mix_liquid_height: mix_liquid_height + none_keys: none_keys + feedback: {} + result: {} + transfer_liquid: + type: DPLiquidHandlerTransferLiquid + goal: + asp_vols: asp_vols + dis_vols: dis_vols + sources: sources + targets: targets + tip_racks: tip_racks + use_channels: use_channels + asp_flow_rates: asp_flow_rates + dis_flow_rates: dis_flow_rates + offsets: offsets + touch_tip: touch_tip + liquid_height: liquid_height + blow_out_air_volume: blow_out_air_volume + spread: spread + is_96_well: is_96_well + mix_stage: mix_stage + mix_times: mix_times + mix_vol: mix_vol + mix_rate: mix_rate + mix_liquid_height: mix_liquid_height + delays: delays + none_keys: none_keys + feedback: {} + result: {} + custom_delay: + type: DPLiquidHandlerCustomDelay + goal: + seconds: seconds + msg: msg + feedback: {} + result: {} + touch_tip: + type: DPLiquidHandlerTouchTip + goal: + targets: targets + feedback: {} + result: {} + mix: + type: DPLiquidHandlerMix + goal: + targets: targets + mix_time: mix_time + mix_vol: mix_vol + height_to_bottom: height_to_bottom + offsets: offsets + mix_rate: mix_rate + none_keys: none_keys + feedback: {} + result: {} + set_tiprack: + type: DPLiquidHandlerSetTiprack + goal: + tip_racks: tip_racks + feedback: {} + result: {} + move_to: + type: DPLiquidHandlerMoveTo + goal: + well: well + dis_to_top: dis_to_top + channel: channel + feedback: {} + result: {} + schema: + type: object + properties: + name: + type: string + description: 物料名 + required: + - name + liquid_handler.revvity: class: module: unilabos.devices.liquid_handling.revvity:Revvity @@ -179,7 +300,7 @@ liquid_handler.revvity: action_value_mappings: run: type: WorkStationRun - goal: + goal: wf_name: file_path params: params resource: resource @@ -187,4 +308,3 @@ liquid_handler.revvity: status: status result: success: success - \ No newline at end of file diff --git a/unilabos/registry/devices/robot_gripper.yaml b/unilabos/registry/devices/robot_gripper.yaml index 04ea338b..bae970ac 100644 --- a/unilabos/registry/devices/robot_gripper.yaml +++ b/unilabos/registry/devices/robot_gripper.yaml @@ -20,7 +20,6 @@ gripper.mock: position: position effort: torque - gripper.misumi_rz: description: Misumi RZ gripper class: @@ -35,4 +34,4 @@ gripper.misumi_rz: command: command feedback: {} result: - success: success \ No newline at end of file + success: success diff --git a/unilabos/registry/devices/sim_nodes.yaml b/unilabos/registry/devices/sim_nodes.yaml new file mode 100644 index 00000000..de4d110e --- /dev/null +++ b/unilabos/registry/devices/sim_nodes.yaml @@ -0,0 +1,5 @@ +lh_joint_publisher: + class: + module: unilabos.devices.ros_dev.liquid_handler_joint_publisher:LiquidHandlerJointPublisher + type: ros2 + diff --git a/unilabos/registry/devices/zhida_hplc.yaml b/unilabos/registry/devices/zhida_hplc.yaml index ba121ae6..1aed8a27 100644 --- a/unilabos/registry/devices/zhida_hplc.yaml +++ b/unilabos/registry/devices/zhida_hplc.yaml @@ -7,7 +7,7 @@ zhida_hplc: status: String action_value_mappings: start: - type: StringSingleInput + type: StrSingleInput goal: string: string feedback: {} diff --git a/unilabos/registry/registry.py b/unilabos/registry/registry.py index feba3fef..dcaaf9f5 100644 --- a/unilabos/registry/registry.py +++ b/unilabos/registry/registry.py @@ -1,3 +1,4 @@ +import io import json import os import sys @@ -20,7 +21,43 @@ class Registry: self.registry_paths = DEFAULT_PATHS.copy() # 使用copy避免修改默认值 if registry_paths: self.registry_paths.extend(registry_paths) - self.device_type_registry = {} + action_type = self._replace_type_with_class( + "ResourceCreateFromOuter", "host_node", f"动作 add_resource_from_outer" + ) + schema = ros_action_to_json_schema(action_type) + self.device_type_registry = { + "host_node": { + "description": "UniLabOS主机节点", + "class": { + "module": "unilabos.ros.nodes.presets.host_node", + "type": "python", + "status_types": {}, + "action_value_mappings": { + "add_resource_from_outer": { + "type": msg_converter_manager.search_class("ResourceCreateFromOuter"), + "goal": { + "resources": "resources", + "device_ids": "device_ids", + "bind_parent_ids": "bind_parent_ids", + "bind_locations": "bind_locations", + "other_calling_params": "other_calling_params", + }, + "feedback": {}, + "result": { + "success": "success" + }, + "schema": schema + } + } + }, + "schema": { + "properties": {}, + "additionalProperties": False, + "type": "object" + }, + "file_path": "/" + } + } self.resource_type_registry = {} self._setup_called = False # 跟踪setup是否已调用 # 其他状态变量 @@ -107,6 +144,7 @@ class Registry: + f"total: {len(files)}" ) current_device_number = len(self.device_type_registry) + 1 + from unilabos.app.web.utils.action_utils import get_yaml_from_goal_type for i, file in enumerate(files): data = yaml.safe_load(open(file, encoding="utf-8")) if data: @@ -131,6 +169,7 @@ class Registry: action_config["type"] = self._replace_type_with_class( action_config["type"], device_id, f"动作 {action_name}" ) + action_config["goal_default"] = yaml.safe_load(io.StringIO(get_yaml_from_goal_type(action_config["type"].Goal))) action_config["schema"] = ros_action_to_json_schema(action_config["type"]) self.device_type_registry.update(data) diff --git a/unilabos/registry/resources/opentrons/deck.yaml b/unilabos/registry/resources/opentrons/deck.yaml index 439da452..652240a5 100644 --- a/unilabos/registry/resources/opentrons/deck.yaml +++ b/unilabos/registry/resources/opentrons/deck.yaml @@ -2,4 +2,7 @@ OTDeck: description: Opentrons deck class: module: pylabrobot.resources.opentrons.deck:OTDeck - type: pylabrobot \ No newline at end of file + type: pylabrobot + model: + type: device + mesh: opentrons_liquid_handler \ No newline at end of file diff --git a/unilabos/registry/resources/opentrons/plates.yaml b/unilabos/registry/resources/opentrons/plates.yaml index f15da8ed..3783bc33 100644 --- a/unilabos/registry/resources/opentrons/plates.yaml +++ b/unilabos/registry/resources/opentrons/plates.yaml @@ -39,6 +39,10 @@ nest_96_wellplate_2ml_deep: class: module: pylabrobot.resources.opentrons.plates:nest_96_wellplate_2ml_deep type: pylabrobot + model: + type: resource + mesh: tecan_nested_tip_rack/meshes/plate.stl + mesh_tf: [0.064, 0.043, 0, -1.5708, 0, 1.5708] nest_96_wellplate_200ul_flat: description: Nest 96 wellplate 200ul flat @@ -51,6 +55,12 @@ nest_96_wellplate_100ul_pcr_full_skirt: class: module: pylabrobot.resources.opentrons.plates:nest_96_wellplate_100ul_pcr_full_skirt type: pylabrobot + model: + type: resource + mesh: tecan_nested_tip_rack/meshes/plate.stl + mesh_tf: [0.064, 0.043, 0, -1.5708, 0, 1.5708] + children_mesh: generic_labware_tube_10_75/meshes/0_base.stl + children_mesh_tf: [0.0018, 0.0018, 0, -1.5708,0, 0] appliedbiosystemsmicroamp_384_wellplate_40ul: description: Applied Biosystems microamp 384 wellplate 40ul diff --git a/unilabos/registry/resources/opentrons/tip_racks.yaml b/unilabos/registry/resources/opentrons/tip_racks.yaml index c5292e2c..a605b55f 100644 --- a/unilabos/registry/resources/opentrons/tip_racks.yaml +++ b/unilabos/registry/resources/opentrons/tip_racks.yaml @@ -63,7 +63,13 @@ opentrons_96_filtertiprack_1000ul: class: module: pylabrobot.resources.opentrons.tip_racks:opentrons_96_filtertiprack_1000ul type: pylabrobot - + model: + type: resource + mesh: tecan_nested_tip_rack/meshes/plate.stl + mesh_tf: [0.064, 0.043, 0, -1.5708, 0, 1.5708] + children_mesh: generic_labware_tube_10_75/meshes/0_base.stl + children_mesh_tf: [0.0018, 0.0018, 0, -1.5708,0, 0] + opentrons_96_filtertiprack_20ul: description: Opentrons 96 filtertiprack 20ul class: diff --git a/unilabos/resources/graphio.py b/unilabos/resources/graphio.py index 71e4aaeb..9621b5de 100644 --- a/unilabos/resources/graphio.py +++ b/unilabos/resources/graphio.py @@ -394,14 +394,14 @@ def resource_ulab_to_plr(resource: dict, plr_model=False) -> "ResourcePLR": return resource_plr -def resource_plr_to_ulab(resource_plr: "ResourcePLR"): +def resource_plr_to_ulab(resource_plr: "ResourcePLR", parent_name: str = None): def resource_plr_to_ulab_inner(d: dict, all_states: dict) -> dict: r = { "id": d["name"], "name": d["name"], "sample_id": None, "children": [resource_plr_to_ulab_inner(child, all_states) for child in d["children"]], - "parent": d["parent_name"] if d["parent_name"] else None, + "parent": d["parent_name"] if d["parent_name"] else parent_name if parent_name else None, "type": "device", # FIXME plr自带的type是python class name "class": d.get("class", ""), "position": ( @@ -417,6 +417,7 @@ def resource_plr_to_ulab(resource_plr: "ResourcePLR"): d = resource_plr.serialize() all_states = resource_plr.serialize_all_state() r = resource_plr_to_ulab_inner(d, all_states) + return r @@ -451,7 +452,8 @@ def initialize_resource(resource_config: dict, lab_registry: dict) -> list[dict] if resource_class_config["type"] == "pylabrobot": resource_plr = RESOURCE(name=resource_config["name"]) - r = resource_plr_to_ulab(resource_plr=resource_plr) + r = resource_plr_to_ulab(resource_plr=resource_plr, parent_name=resource_config.get("parent", None)) + # r = resource_plr_to_ulab(resource_plr=resource_plr) if resource_config.get("position") is not None: r["position"] = resource_config["position"] r = tree_to_list([r]) @@ -475,8 +477,10 @@ def initialize_resources(resources_config) -> list[dict]: """ from unilabos.registry.registry import lab_registry - resources = [] for resource_config in resources_config: + if resource_config["parent"] == "tip_rack" or resource_config["parent"] == "plate_well": + continue resources.extend(initialize_resource(resource_config, lab_registry)) + return resources diff --git a/unilabos/resources/registry.py b/unilabos/resources/registry.py index 435edb68..fae72622 100644 --- a/unilabos/resources/registry.py +++ b/unilabos/resources/registry.py @@ -1,5 +1,5 @@ -import sys - +import traceback +from unilabos.utils.log import logger resource_schema = { "workstation": {"type": "object", "properties": {}}, @@ -132,7 +132,8 @@ def add_schema(resources_config: list[dict]) -> list[dict]: try: if type(resource["children"][0]) == dict: resource["children"] = add_schema(resource["children"]) - except: - sys.exit(0) + except Exception as ex: + logger.error("添加物料schema时出错") + traceback.print_exc() return resources_config diff --git a/unilabos/ros/device_node_wrapper.py b/unilabos/ros/device_node_wrapper.py index 1697a9e8..f6d071f5 100644 --- a/unilabos/ros/device_node_wrapper.py +++ b/unilabos/ros/device_node_wrapper.py @@ -18,6 +18,7 @@ class ROS2DeviceNodeWrapper(ROS2DeviceNode): def ros2_device_node( cls: Type[T], + device_config: Optional[Dict[str, Any]] = None, status_types: Optional[Dict[str, Any]] = None, action_value_mappings: Optional[Dict[str, Any]] = None, hardware_interface: Optional[Dict[str, Any]] = None, @@ -30,6 +31,7 @@ def ros2_device_node( cls: 要封装的设备类 status_types: 需要发布的状态和传感器信息,每个(PROP: TYPE),PROP应该匹配cls.PROP或cls.get_PROP(), TYPE应该是ROS2消息类型。默认为{}。 + device_config: 初始化时的config。 action_value_mappings: 设备动作。默认为{}。 每个(ACTION: {'type': CMD_TYPE, 'goal': {FIELD: PROP}, 'feedback': {FIELD: PROP}, 'result': {FIELD: PROP}}), hardware_interface: 硬件接口配置。默认为{"name": "hardware_interface", "write": "send_command", "read": "read_data", "extra_info": []}。 @@ -42,6 +44,8 @@ def ros2_device_node( # 从属性中自动发现可发布状态 if status_types is None: status_types = {} + if device_config is None: + device_config = {} if action_value_mappings is None: action_value_mappings = {} if hardware_interface is None: @@ -73,6 +77,7 @@ def ros2_device_node( "__init__": lambda self, *args, **kwargs: init_wrapper( self, driver_class=cls, + device_config=device_config, status_types=status_types, action_value_mappings=action_value_mappings, hardware_interface=hardware_interface, diff --git a/unilabos/ros/initialize_device.py b/unilabos/ros/initialize_device.py index 730caa13..ed667f1e 100644 --- a/unilabos/ros/initialize_device.py +++ b/unilabos/ros/initialize_device.py @@ -1,9 +1,9 @@ -import rclpy -from rclpy.node import Node +import copy from typing import Optional + from unilabos.registry.registry import lab_registry -from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, DeviceInitError from unilabos.ros.device_node_wrapper import ros2_device_node +from unilabos.ros.nodes.base_device_node import ROS2DeviceNode, DeviceInitError from unilabos.utils import logger from unilabos.utils.import_manager import default_manager @@ -22,17 +22,21 @@ def initialize_device_from_dict(device_id, device_config) -> Optional[ROS2Device None """ d = None + original_device_config = copy.deepcopy(device_config) device_class_config = device_config["class"] if isinstance(device_class_config, str): # 如果是字符串,则直接去lab_registry中查找,获取class if device_class_config not in lab_registry.device_type_registry: raise ValueError(f"Device class {device_class_config} not found.") device_class_config = device_config["class"] = lab_registry.device_type_registry[device_class_config]["class"] + else: + raise ValueError("不再支持class为字典传入,class必须为注册表中已经提供的设备,您可以新增注册表并通过--registry传入") if isinstance(device_class_config, dict): DEVICE = default_manager.get_class(device_class_config["module"]) # 不管是ros2的实例,还是python的,都必须包一次,除了HostNode DEVICE = ros2_device_node( DEVICE, status_types=device_class_config.get("status_types", {}), + device_config=original_device_config, action_value_mappings=device_class_config.get("action_value_mappings", {}), hardware_interface=device_class_config.get( "hardware_interface", diff --git a/unilabos/ros/main_slave_run.py b/unilabos/ros/main_slave_run.py index 9ac96748..c4c5a172 100644 --- a/unilabos/ros/main_slave_run.py +++ b/unilabos/ros/main_slave_run.py @@ -2,9 +2,13 @@ import copy import json import os import threading +import time from typing import Optional, Dict, Any, List import rclpy +from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher +from unilabos.ros.nodes.presets.resource_mesh_manager import ResourceMeshManager +from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker from unilabos_msgs.msg import Resource # type: ignore from unilabos_msgs.srv import ResourceAdd, SerialCommand # type: ignore from rclpy.executors import MultiThreadedExecutor @@ -40,17 +44,19 @@ def exit() -> None: def main( devices_config: Dict[str, Any] = {}, - resources_config={}, + resources_config: list=[], graph: Optional[Dict[str, Any]] = None, controllers_config: Dict[str, Any] = {}, bridges: List[Any] = [], - args: List[str] = ["--log-level", "debug"], + visual: str = "disable", + resources_mesh_config: dict = {}, + rclpy_init_args: List[str] = ["--log-level", "debug"], discovery_interval: float = 5.0, ) -> None: """主函数""" - rclpy.init(args=args) - rclpy.__executor = executor = MultiThreadedExecutor() + rclpy.init(args=rclpy_init_args) + executor = rclpy.__executor = MultiThreadedExecutor() # 创建主机节点 host_node = HostNode( "host_node", @@ -62,11 +68,26 @@ def main( discovery_interval, ) + if visual != "disable": + resource_mesh_manager = ResourceMeshManager( + resources_mesh_config, + resources_config, + resource_tracker= DeviceNodeResourceTracker(), + device_id = 'resource_mesh_manager', + ) + joint_republisher = JointRepublisher( + 'joint_republisher', + DeviceNodeResourceTracker() + ) + + executor.add_node(resource_mesh_manager) + executor.add_node(joint_republisher) + thread = threading.Thread(target=executor.spin, daemon=True, name="host_executor_thread") thread.start() while True: - input() + time.sleep(1) def slave( @@ -75,11 +96,16 @@ def slave( graph: Optional[Dict[str, Any]] = None, controllers_config: Dict[str, Any] = {}, bridges: List[Any] = [], - args: List[str] = ["--log-level", "debug"], + visual: str = "disable", + resources_mesh_config: dict = {}, + rclpy_init_args: List[str] = ["--log-level", "debug"], ) -> None: """从节点函数""" - rclpy.init(args=args) - rclpy.__executor = executor = MultiThreadedExecutor() + if not rclpy.ok(): + rclpy.init(args=rclpy_init_args) + executor = rclpy.__executor + if not executor: + executor = rclpy.__executor = MultiThreadedExecutor() devices_config_copy = copy.deepcopy(devices_config) for device_id, device_config in devices_config.items(): d = initialize_device_from_dict(device_id, device_config) @@ -94,6 +120,21 @@ def slave( n = Node(f"slaveMachine_{BasicConfig.machine_name}", parameter_overrides=[]) executor.add_node(n) + if visual != "disable": + resource_mesh_manager = ResourceMeshManager( + resources_mesh_config, + resources_config, + resource_tracker= DeviceNodeResourceTracker(), + device_id = 'resource_mesh_manager', + ) + joint_republisher = JointRepublisher( + 'joint_republisher', + DeviceNodeResourceTracker() + ) + + executor.add_node(resource_mesh_manager) + executor.add_node(joint_republisher) + thread = threading.Thread(target=executor.spin, daemon=True, name="slave_executor_thread") thread.start() @@ -112,7 +153,7 @@ def slave( logger.info(f"Slave node info updated.") rclient = n.create_client(ResourceAdd, "/resources/add") - rclient.wait_for_service() # FIXME 可能一直等待,加一个参数 + rclient.wait_for_service() request = ResourceAdd.Request() request.resources = [convert_to_ros_msg(Resource, resource) for resource in resources_config] @@ -120,7 +161,7 @@ def slave( logger.info(f"Slave resource added.") while True: - input() + time.sleep(1) if __name__ == "__main__": main() diff --git a/unilabos/ros/msgs/message_converter.py b/unilabos/ros/msgs/message_converter.py index 5e87fce5..4f85a113 100644 --- a/unilabos/ros/msgs/message_converter.py +++ b/unilabos/ros/msgs/message_converter.py @@ -133,15 +133,15 @@ _msg_converter: Dict[Type, Any] = { String: lambda x: String(data=str(x)), Point: lambda x: Point(x=x.x, y=x.y, z=x.z), Resource: lambda x: Resource( - id=x["id"], - name=x["name"], + id=x.get("id", ""), + name=x.get("name", ""), sample_id=x.get("sample_id", "") or "", children=list(x.get("children", [])), parent=x.get("parent", "") or "", - type=x["type"], - category=x.get("class", "") or x["type"], + type=x.get("type", ""), + category=x.get("class", "") or x.get("type", ""), pose=( - Pose(position=Point(x=float(x["position"]["x"]), y=float(x["position"]["y"]), z=float(x["position"]["z"]))) + Pose(position=Point(x=float(x.get("position", {}).get("x", 0)), y=float(x.get("position", {}).get("y", 0)), z=float(x.get("position", {}).get("z", 0)))) if x.get("position", None) is not None else Pose() ), @@ -331,16 +331,27 @@ def convert_to_ros_msg(ros_msg_type: Union[Type, Any], obj: Any) -> Any: ros_msg = ros_msg_type() if isinstance(ros_msg_type, type) else ros_msg_type # 提取数据 - data = _extract_data(obj) + extract_data = dict(_extract_data(obj)) # 转换数据到ROS消息 - for key, value in data.items(): + for ind, data in enumerate(ros_msg.get_fields_and_field_types().items()): + key, type_name = data + if key not in extract_data: + continue + value = extract_data[key] if hasattr(ros_msg, key): attr = getattr(ros_msg, key) if isinstance(attr, (float, int, str, bool)): setattr(ros_msg, key, value) elif isinstance(attr, (list, tuple)) and isinstance(value, Iterable): - setattr(ros_msg, key, list(value)) + td = ros_msg.SLOT_TYPES[ind].value_type + if isinstance(td, NamespacedType): + target_class = msg_converter_manager.get_class(f"{'.'.join(td.namespaces)}.{td.name}") + setattr(ros_msg, key, [convert_to_ros_msg(target_class, v) for v in value]) + else: + setattr(ros_msg, key, []) # FIXME + elif "array.array" in str(type(attr)): + setattr(ros_msg, key, value) else: nested_ros_msg = convert_to_ros_msg(type(attr)(), value) setattr(ros_msg, key, nested_ros_msg) @@ -566,6 +577,7 @@ basic_type_map = { 'float32': {'type': 'number'}, 'float64': {'type': 'number'}, 'string': {'type': 'string'}, + 'boolean': {'type': 'boolean'}, 'char': {'type': 'string', 'maxLength': 1}, 'byte': {'type': 'integer', 'minimum': 0, 'maximum': 255}, } diff --git a/unilabos/ros/nodes/base_device_node.py b/unilabos/ros/nodes/base_device_node.py index 7e032064..078cb470 100644 --- a/unilabos/ros/nodes/base_device_node.py +++ b/unilabos/ros/nodes/base_device_node.py @@ -10,13 +10,16 @@ import asyncio import rclpy from rclpy.node import Node -from rclpy.action import ActionServer +from rclpy.action import ActionServer, ActionClient from rclpy.action.server import ServerGoalHandle from rclpy.client import Client from rclpy.callback_groups import ReentrantCallbackGroup from rclpy.service import Service +from unilabos_msgs.action import SendCmd +from unilabos_msgs.srv._serial_command import SerialCommand_Request, SerialCommand_Response -from unilabos.resources.graphio import convert_resources_to_type, convert_resources_from_type +from unilabos.resources.graphio import convert_resources_to_type, convert_resources_from_type, resource_ulab_to_plr, \ + initialize_resources from unilabos.ros.msgs.message_converter import ( convert_to_ros_msg, convert_from_ros_msg, @@ -101,6 +104,7 @@ def init_wrapper( self, device_id: str, driver_class: type[T], + device_config: Dict[str, Any], status_types: Dict[str, Any], action_value_mappings: Dict[str, Any], hardware_interface: Dict[str, Any], @@ -118,6 +122,7 @@ def init_wrapper( children = [] kwargs["device_id"] = device_id kwargs["driver_class"] = driver_class + kwargs["device_config"] = device_config kwargs["driver_params"] = driver_params kwargs["status_types"] = status_types kwargs["action_value_mappings"] = action_value_mappings @@ -302,10 +307,77 @@ class BaseROS2DeviceNode(Node, Generic[T]): res.response = "" return res + def append_resource(req: SerialCommand_Request, res: SerialCommand_Response): + # 物料传输到对应的node节点 + rclient = self.create_client(ResourceAdd, "/resources/add") + rclient.wait_for_service() + request = ResourceAdd.Request() + command_json = json.loads(req.command) + namespace = command_json["namespace"] + bind_parent_id = command_json["bind_parent_id"] + edge_device_id = command_json["edge_device_id"] + location = command_json["bind_location"] + other_calling_param = command_json["other_calling_param"] + resources = command_json["resource"] + initialize_full = other_calling_param.pop("initialize_full", False) + # 本地拿到这个物料,可能需要先做初始化? + if isinstance(resources, list): + if initialize_full: + resources = initialize_resources(resources) + request.resources = [convert_to_ros_msg(Resource, resource) for resource in resources] + else: + if initialize_full: + resources = initialize_resources([resources]) + request.resources = [convert_to_ros_msg(Resource, resources)] + response = rclient.call(request) + # 应该先add_resource了 + res.response = "OK" + # 接下来该根据bind_parent_id进行assign了,目前只有plr可以进行assign,不然没有办法输入到物料系统中 + resource = self.resource_tracker.figure_resource({"name": bind_parent_id}) + request.resources = [convert_to_ros_msg(Resource, resources)] + + try: + from pylabrobot.resources.resource import Resource as ResourcePLR + from pylabrobot.resources.deck import Deck + from pylabrobot.resources import Coordinate + from pylabrobot.resources import OTDeck + contain_model = not isinstance(resource, Deck) + if isinstance(resource, ResourcePLR): + # resources.list() + plr_instance = resource_ulab_to_plr(resources, contain_model) + if isinstance(resource, OTDeck) and "slot" in other_calling_param: + resource.assign_child_at_slot(plr_instance, **other_calling_param) + resource.assign_child_resource(plr_instance, Coordinate(location["x"], location["y"], location["z"]), **other_calling_param) + # 发送给ResourceMeshManager + action_client = ActionClient( + self, SendCmd, "/devices/resource_mesh_manager/add_resource_mesh", callback_group=self.callback_group + ) + goal = SendCmd.Goal() + goal.command = json.dumps({ + "resources": resources, + "bind_parent_id": bind_parent_id, + }) + future = action_client.send_goal_async(goal, goal_uuid=uuid.uuid4()) + + def done_cb(*args): + self.lab_logger().info(f"向meshmanager发送新增resource完成") + + future.add_done_callback(done_cb) + except ImportError: + self.lab_logger().error("Host请求添加物料时,本环境并不存在pylabrobot") + except Exception as e: + self.lab_logger().error("Host请求添加物料时出错") + self.lab_logger().error(traceback.format_exc()) + return res + + # noinspection PyTypeChecker self._service_server: Dict[str, Service] = { "query_host_name": self.create_service( SerialCommand, f"/srv{self.namespace}/query_host_name", query_host_name_cb, callback_group=self.callback_group ), + "append_resource": self.create_service( + SerialCommand, f"/srv{self.namespace}/append_resource", append_resource, callback_group=self.callback_group + ), } # 向全局在线设备注册表添加设备信息 @@ -437,26 +509,36 @@ class BaseROS2DeviceNode(Node, Generic[T]): action_kwargs = convert_from_ros_msg_with_mapping(goal, action_value_mapping["goal"]) self.lab_logger().debug(f"接收到原始目标: {action_kwargs}") - - # 向Host查询物料当前状态 - for k, v in goal.get_fields_and_field_types().items(): - if v in ["unilabos_msgs/Resource", "sequence"]: - self.lab_logger().info(f"查询资源状态: Key: {k} Type: {v}") - try: - r = ResourceGet.Request() - r.id = action_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else action_kwargs[k][0]["id"] - r.with_children = True - response = await self._resource_clients["resource_get"].call_async(r) - except Exception: - logger.error(f"资源查询失败,默认使用本地资源") - # 删除对response.resources的检查,因为它总是存在 - resources_list = [convert_from_ros_msg(rs) for rs in response.resources] # type: ignore # FIXME - self.lab_logger().debug(f"资源查询结果: {len(resources_list)} 个资源") - type_hint = action_paramtypes[k] - final_type = get_type_class(type_hint) - # 判断 ACTION 是否需要特殊的物料类型如 pylabrobot.resources.Resource,并做转换 - final_resource = convert_resources_to_type(resources_list, final_type) - action_kwargs[k] = self.resource_tracker.figure_resource(final_resource) + # 向Host查询物料当前状态,如果是host本身的增加物料的请求,则直接跳过 + if action_name != "add_resource_from_outer": + for k, v in goal.get_fields_and_field_types().items(): + if v in ["unilabos_msgs/Resource", "sequence"]: + self.lab_logger().info(f"查询资源状态: Key: {k} Type: {v}") + current_resources = [] + try: + if len(action_kwargs[k]) > 1: + for i in action_kwargs[k]: + r = ResourceGet.Request() + r.id = i["id"] + r.with_children = True + response = await self._resource_clients["resource_get"].call_async(r) + current_resources.extend(response.resources) + else: + r = ResourceGet.Request() + r.id = action_kwargs[k]["id"] if v == "unilabos_msgs/Resource" else action_kwargs[k][0]["id"] + r.with_children = True + response = await self._resource_clients["resource_get"].call_async(r) + current_resources.extend(response.resources) + except Exception: + logger.error(f"资源查询失败,默认使用本地资源") + # 删除对response.resources的检查,因为它总是存在 + resources_list = [convert_from_ros_msg(rs) for rs in current_resources] # type: ignore # FIXME + self.lab_logger().debug(f"资源查询结果: {len(resources_list)} 个资源") + type_hint = action_paramtypes[k] + final_type = get_type_class(type_hint) + # 判断 ACTION 是否需要特殊的物料类型如 pylabrobot.resources.Resource,并做转换 + final_resource = [convert_resources_to_type([i], final_type)[0] for i in resources_list] + action_kwargs[k] = self.resource_tracker.figure_resource(final_resource) self.lab_logger().info(f"准备执行: {action_kwargs}, 函数: {ACTION.__name__}") time_start = time.time() @@ -527,27 +609,28 @@ class BaseROS2DeviceNode(Node, Generic[T]): del future # 向Host更新物料当前状态 - for k, v in goal.get_fields_and_field_types().items(): - if v not in ["unilabos_msgs/Resource", "sequence"]: - continue - self.lab_logger().info(f"更新资源状态: {k}") - r = ResourceUpdate.Request() - # 仅当action_kwargs[k]不为None时尝试转换 - akv = action_kwargs[k] - apv = action_paramtypes[k] - final_type = get_type_class(apv) - if final_type is None: - continue - try: - r.resources = [ - convert_to_ros_msg(Resource, self.resource_tracker.root_resource(rs)) - for rs in convert_resources_from_type(akv, final_type) # type: ignore # FIXME # 考虑反查到最大的 - ] - response = await self._resource_clients["resource_update"].call_async(r) - self.lab_logger().debug(f"资源更新结果: {response}") - except Exception as e: - self.lab_logger().error(f"资源更新失败: {e}") - self.lab_logger().error(traceback.format_exc()) + if action_name != "add_resource_from_outer": + for k, v in goal.get_fields_and_field_types().items(): + if v not in ["unilabos_msgs/Resource", "sequence"]: + continue + self.lab_logger().info(f"更新资源状态: {k}") + r = ResourceUpdate.Request() + # 仅当action_kwargs[k]不为None时尝试转换 + akv = action_kwargs[k] + apv = action_paramtypes[k] + final_type = get_type_class(apv) + if final_type is None: + continue + try: + r.resources = [ + convert_to_ros_msg(Resource, self.resource_tracker.root_resource(rs)) + for rs in convert_resources_from_type(akv, final_type) # type: ignore # FIXME # 考虑反查到最大的 + ] + response = await self._resource_clients["resource_update"].call_async(r) + self.lab_logger().debug(f"资源更新结果: {response}") + except Exception as e: + self.lab_logger().error(f"资源更新失败: {e}") + self.lab_logger().error(traceback.format_exc()) # 发布结果 goal_handle.succeed() @@ -627,6 +710,7 @@ class ROS2DeviceNode: self, device_id: str, driver_class: Type[T], + device_config: Dict[str, Any], driver_params: Dict[str, Any], status_types: Dict[str, Any], action_value_mappings: Dict[str, Any], @@ -641,6 +725,8 @@ class ROS2DeviceNode: Args: device_id: 设备标识符 driver_class: 设备类 + device_config: 原始初始化的json + driver_params: driver初始化的参数 status_types: 状态类型映射 action_value_mappings: 动作值映射 hardware_interface: 硬件接口配置 @@ -657,11 +743,12 @@ class ROS2DeviceNode: # 保存设备类是否支持异步上下文 self._has_async_context = hasattr(driver_class, "__aenter__") and hasattr(driver_class, "__aexit__") self._driver_class = driver_class + self.device_config = device_config self.driver_is_ros = driver_is_ros self.resource_tracker = DeviceNodeResourceTracker() # use_pylabrobot_creator 使用 cls的包路径检测 - use_pylabrobot_creator = driver_class.__module__.startswith("pylabrobot") + use_pylabrobot_creator = driver_class.__module__.startswith("pylabrobot") or driver_class.__name__ == "DPLiquidHandler" # TODO: 要在创建之前预先请求服务器是否有当前id的物料,放到resource_tracker中,让pylabrobot进行创建 # 创建设备类实例 diff --git a/unilabos/ros/nodes/presets/host_node.py b/unilabos/ros/nodes/presets/host_node.py index 5a739773..4338872a 100644 --- a/unilabos/ros/nodes/presets/host_node.py +++ b/unilabos/ros/nodes/presets/host_node.py @@ -7,11 +7,13 @@ import uuid from typing import Optional, Dict, Any, List, ClassVar, Set from action_msgs.msg import GoalStatus -from unilabos_msgs.msg import Resource # type: ignore -from unilabos_msgs.srv import ResourceAdd, ResourceGet, ResourceDelete, ResourceUpdate, ResourceList, SerialCommand # type: ignore +from geometry_msgs.msg import Point from rclpy.action import ActionClient, get_action_server_names_and_types_by_node from rclpy.callback_groups import ReentrantCallbackGroup from rclpy.service import Service +from unilabos_msgs.msg import Resource # type: ignore +from unilabos_msgs.srv import ResourceAdd, ResourceGet, ResourceDelete, ResourceUpdate, ResourceList, \ + SerialCommand # type: ignore from unique_identifier_msgs.msg import UUID from unilabos.registry.registry import lab_registry @@ -23,11 +25,9 @@ from unilabos.ros.msgs.message_converter import ( convert_from_ros_msg, convert_to_ros_msg, msg_converter_manager, - ros_action_to_json_schema, ) from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode, ROS2DeviceNode, DeviceNodeResourceTracker from unilabos.ros.nodes.presets.controller_node import ControllerNode -from unilabos.utils.type_check import TypeEncoder class HostNode(BaseROS2DeviceNode): @@ -50,7 +50,7 @@ class HostNode(BaseROS2DeviceNode): self, device_id: str, devices_config: Dict[str, Any], - resources_config: Any, + resources_config: list, physical_setup_graph: Optional[Dict[str, Any]] = None, controllers_config: Optional[Dict[str, Any]] = None, bridges: Optional[List[Any]] = None, @@ -76,7 +76,7 @@ class HostNode(BaseROS2DeviceNode): driver_instance=self, device_id=device_id, status_types={}, - action_value_mappings={}, + action_value_mappings=lab_registry.device_type_registry["host_node"]["class"]["action_value_mappings"], hardware_interface={}, print_publish=False, resource_tracker=DeviceNodeResourceTracker(), # host node并不是通过initialize 包一层传进来的 @@ -97,15 +97,13 @@ class HostNode(BaseROS2DeviceNode): self.bridges = bridges # 创建设备、动作客户端和目标存储 - self.devices_names: Dict[str, str] = {} # 存储设备名称和命名空间的映射 + self.devices_names: Dict[str, str] = {device_id: self.namespace} # 存储设备名称和命名空间的映射 self.devices_instances: Dict[str, ROS2DeviceNode] = {} # 存储设备实例 self.device_machine_names: Dict[str, str] = {device_id: "本地", } # 存储设备ID到机器名称的映射 self._action_clients: Dict[str, ActionClient] = {} # 用来存储多个ActionClient实例 - self._action_value_mappings: Dict[str, Dict] = ( - {} - ) # 用来存储多个ActionClient的type, goal, feedback, result的变量名映射关系 + self._action_value_mappings: Dict[str, Dict] = {} # 用来存储多个ActionClient的type, goal, feedback, result的变量名映射关系 self._goals: Dict[str, Any] = {} # 用来存储多个目标的状态 - self._online_devices: Set[str] = set() # 用于跟踪在线设备 + self._online_devices: Set[str] = {f"{self.namespace}/{device_id}"} # 用于跟踪在线设备 self._last_discovery_time = 0.0 # 上次设备发现的时间 self._discovery_lock = threading.Lock() # 设备发现的互斥锁 self._subscribed_topics = set() # 用于跟踪已订阅的话题 @@ -259,16 +257,41 @@ class HostNode(BaseROS2DeviceNode): self.lab_logger().debug(f"[Host Node] Created ActionClient (Discovery): {action_id}") action_name = action_id[len(namespace) + 1:] edge_device_id = namespace[9:] - from unilabos.app.mq import mqtt_client - info_with_schema = ros_action_to_json_schema(action_type) - mqtt_client.publish_actions(action_name, { - "device_id": edge_device_id, - "action_name": action_name, - "schema": info_with_schema, - }) + # from unilabos.app.mq import mqtt_client + # info_with_schema = ros_action_to_json_schema(action_type) + # mqtt_client.publish_actions(action_name, { + # "device_id": edge_device_id, + # "device_type": "", + # "action_name": action_name, + # "schema": info_with_schema, + # }) except Exception as e: self.lab_logger().error(f"[Host Node] Failed to create ActionClient for {action_id}: {str(e)}") + def add_resource_from_outer(self, resources: list["Resource"], device_ids: list[str], bind_parent_ids: list[str], bind_locations: list[Point], other_calling_params: list[str]): + for resource, device_id, bind_parent_id, bind_location, other_calling_param in zip(resources, device_ids, bind_parent_ids, bind_locations, other_calling_params): + # 这里要求device_id传入必须是edge_device_id + namespace = "/devices/" + device_id + srv_address = f"/srv{namespace}/append_resource" + sclient = self.create_client(SerialCommand, srv_address) + sclient.wait_for_service() + request = SerialCommand.Request() + request.command = json.dumps({ + "resource": resource, + "namespace": namespace, + "edge_device_id": device_id, + "bind_parent_id": bind_parent_id, + "bind_location": { + "x": bind_location.x, + "y": bind_location.y, + "z": bind_location.z, + }, + "other_calling_param": json.loads(other_calling_param) if other_calling_param else {}, + }, ensure_ascii=False) + response = sclient.call(request) + pass + pass + def initialize_device(self, device_id: str, device_config: Dict[str, Any]) -> None: """ 根据配置初始化设备, @@ -297,13 +320,14 @@ class HostNode(BaseROS2DeviceNode): action_type = action_value_mapping["type"] self._action_clients[action_id] = ActionClient(self, action_type, action_id) self.lab_logger().debug(f"[Host Node] Created ActionClient (Local): {action_id}") # 子设备再创建用的是Discover发现的 - from unilabos.app.mq import mqtt_client - info_with_schema = ros_action_to_json_schema(action_type) - mqtt_client.publish_actions(action_name, { - "device_id": device_id, - "action_name": action_name, - "schema": info_with_schema, - }) + # from unilabos.app.mq import mqtt_client + # info_with_schema = ros_action_to_json_schema(action_type) + # mqtt_client.publish_actions(action_name, { + # "device_id": device_id, + # "device_type": device_config["class"], + # "action_name": action_name, + # "schema": info_with_schema, + # }) else: self.lab_logger().warning(f"[Host Node] ActionClient {action_id} already exists.") device_key = f"{self.devices_names[device_id]}/{device_id}" # 这里不涉及二级device_id @@ -619,7 +643,8 @@ class HostNode(BaseROS2DeviceNode): self.lab_logger().debug(f"[Host Node-Resource] Retrieved from bridge: {len(r)} resources") except Exception as e: self.lab_logger().error(f"[Host Node-Resource] Error retrieving from bridge: {str(e)}") - r = [] + r = [resource for resource in self.resources_config if resource.get("id") == request.id] + self.lab_logger().warning(f"[Host Node-Resource] Retrieved from local: {len(r)} resources") else: # 本地物料服务,根据 id 查询物料 r = [resource for resource in self.resources_config if resource.get("id") == request.id] diff --git a/unilabos/ros/nodes/presets/joint_republisher.py b/unilabos/ros/nodes/presets/joint_republisher.py new file mode 100644 index 00000000..b731acc7 --- /dev/null +++ b/unilabos/ros/nodes/presets/joint_republisher.py @@ -0,0 +1,60 @@ +import rclpy,json +from rclpy.node import Node +from sensor_msgs.msg import JointState +from std_msgs.msg import String +from rclpy.callback_groups import ReentrantCallbackGroup +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode + +class JointRepublisher(BaseROS2DeviceNode): + def __init__(self,device_id,resource_tracker): + super().__init__( + driver_instance=self, + device_id=device_id, + status_types={}, + action_value_mappings={}, + hardware_interface={}, + print_publish=False, + resource_tracker=resource_tracker, + ) + + # print('-'*20,device_id) + self.joint_repub = self.create_publisher(String,f'joint_state_repub',10) + # 创建订阅者 + self.create_subscription( + JointState, + '/joint_states', + self.listener_callback, + 10, + callback_group=ReentrantCallbackGroup() + ) + self.msg = String() + + def listener_callback(self, msg:JointState): + + try: + json_dict = {} + json_dict["name"] = list(msg.name) + json_dict["position"] = list(msg.position) + json_dict["velocity"] = list(msg.velocity) + json_dict["effort"] = list(msg.effort) + + self.msg.data = str(json_dict) + self.joint_repub.publish(self.msg) + # print('-'*20) + # print(self.msg.data) + + except Exception as e: + print(e) + + +def main(): + + rclpy.init() + subscriber = JointRepublisher() + rclpy.spin(subscriber) + subscriber.destroy_node() + rclpy.shutdown() + + +if __name__ == '__main__': + main() diff --git a/unilabos/ros/nodes/presets/resource_mesh_manager.py b/unilabos/ros/nodes/presets/resource_mesh_manager.py new file mode 100644 index 00000000..c1495818 --- /dev/null +++ b/unilabos/ros/nodes/presets/resource_mesh_manager.py @@ -0,0 +1,1139 @@ +from pathlib import Path +import time +import rclpy,json +from rclpy.node import Node +from std_msgs.msg import String,Header +import numpy as np +from moveit_msgs.srv import GetPlanningScene, ApplyPlanningScene +from rclpy.callback_groups import ReentrantCallbackGroup +from rclpy.qos import QoSProfile, QoSDurabilityPolicy, QoSReliabilityPolicy, QoSHistoryPolicy +from moveit_msgs.msg import CollisionObject, AttachedCollisionObject, AllowedCollisionEntry, RobotState, PlanningScene +from shape_msgs.msg import Mesh, MeshTriangle, SolidPrimitive +from geometry_msgs.msg import Pose, PoseStamped, Point, Quaternion, TransformStamped +from rclpy.callback_groups import ReentrantCallbackGroup +from rclpy.qos import QoSProfile, QoSDurabilityPolicy, QoSReliabilityPolicy, QoSHistoryPolicy +from rclpy.task import Future +import copy +from typing import Tuple, Optional, Union, Any, List +from tf_transformations import quaternion_from_euler +from tf2_ros import TransformBroadcaster, Buffer, TransformListener +from rclpy.action import ActionServer +from unilabos_msgs.action import SendCmd +from rclpy.action.server import ServerGoalHandle +from unilabos.ros.nodes.base_device_node import BaseROS2DeviceNode,DeviceNodeResourceTracker +from unilabos.resources.graphio import initialize_resources +from unilabos.registry.registry import lab_registry + +class ResourceMeshManager(BaseROS2DeviceNode): + def __init__(self, resource_model: dict, resource_config: list,resource_tracker, device_id: str = "resource_mesh_manager", rate=50): + """初始化资源网格管理器节点 + + Args: + resource_model (dict): 资源模型字典,包含资源的3D模型信息 + resource_config (dict): 资源配置字典,包含资源的配置信息 + device_id (str): 节点名称 + """ + super().__init__( + driver_instance=self, + device_id=device_id, + status_types={}, + action_value_mappings={}, + hardware_interface={}, + print_publish=False, + resource_tracker=resource_tracker, + ) + + self.resource_model = resource_model + self.resource_config_dict = {item['id']: item for item in resource_config} + self.move_group_ready = False + self.resource_tf_dict = {} + self.tf_broadcaster = TransformBroadcaster(self) + self.tf_buffer = Buffer() + self.tf_listener = TransformListener(self.tf_buffer, self) + self.rate = rate + self.zero_count = 0 + + self.old_resource_pose = {} + self.__planning_scene = PlanningScene() + self.__old_planning_scene = None + self.__old_allowed_collision_matrix = None + self.mesh_path = Path(__file__).parent.parent.parent.parent.absolute() + + callback_group = ReentrantCallbackGroup() + self._get_planning_scene_service = self.create_client( + srv_type=GetPlanningScene, + srv_name="/get_planning_scene", + qos_profile=QoSProfile( + durability=QoSDurabilityPolicy.VOLATILE, + reliability=QoSReliabilityPolicy.RELIABLE, + history=QoSHistoryPolicy.KEEP_LAST, + depth=1, + ), + callback_group=callback_group, + ) + + # Create a service for applying the planning scene + self._apply_planning_scene_service = self.create_client( + srv_type=ApplyPlanningScene, + srv_name="/apply_planning_scene", + qos_profile=QoSProfile( + durability=QoSDurabilityPolicy.VOLATILE, + reliability=QoSReliabilityPolicy.RELIABLE, + history=QoSHistoryPolicy.KEEP_LAST, + depth=1, + ), + callback_group=callback_group, + ) + + self.resource_pose_publisher = self.create_publisher( + String, f"resource_pose", 10 + ) + self.__collision_object_publisher = self.create_publisher( + CollisionObject, "/collision_object", 10 + ) + self.__attached_collision_object_publisher = self.create_publisher( + AttachedCollisionObject, "/attached_collision_object", 10 + ) + + # 创建一个Action Server用于修改resource_tf_dict + self._action_server = ActionServer( + self, + SendCmd, + f"tf_update", + self.tf_update, + callback_group=callback_group + ) + + # 创建一个Action Server用于添加新的资源模型与resource_tf_dict + self._add_resource_mesh_action_server = ActionServer( + self, + SendCmd, + f"add_resource_mesh", + self.add_resource_mesh_callback, + callback_group=callback_group + ) + + self.resource_tf_dict = self.resource_mesh_setup(self.resource_config_dict) + self.create_timer(1/self.rate, self.publish_resource_tf) + self.create_timer(1/self.rate, self.check_resource_pose_changes) + + def check_move_group_ready(self): + """检查move_group节点是否已初始化完成""" + + # 获取当前可用的节点列表 + + tf_ready = self.tf_buffer.can_transform("world", next(iter(self.resource_tf_dict.keys())), rclpy.time.Time(),rclpy.duration.Duration(seconds=2)) + + # if tf_ready: + if self._get_planning_scene_service.service_is_ready() and self._apply_planning_scene_service.service_is_ready() and tf_ready: + self.move_group_ready = True + self.publish_resource_tf() + self.add_resource_collision_meshes(self.resource_tf_dict) + + # time.sleep(1) + + def add_resource_mesh_callback(self, goal_handle : ServerGoalHandle): + tf_update_msg = goal_handle.request + try: + self.add_resource_mesh(tf_update_msg.command) + except Exception as e: + self.get_logger().error(f"添加资源失败: {e}") + goal_handle.abort() + return SendCmd.Result(success=False) + goal_handle.succeed() + return SendCmd.Result(success=True) + + def add_resource_mesh(self,resource_config_str:str): + """刷新资源配置""" + + registry = lab_registry + resource_config = json.loads(resource_config_str) + + if resource_config['id'] in self.resource_config_dict: + self.get_logger().info(f'资源 {resource_config["id"]} 已存在') + return + if resource_config['class'] in registry.resource_type_registry.keys(): + model_config = registry.resource_type_registry[resource_config['class']]['model'] + if model_config['type'] == 'resource': + self.resource_model[resource_config['id']] = { + 'mesh': f"{str(self.mesh_path)}/device_mesh/resources/{model_config['mesh']}", + 'mesh_tf': model_config['mesh_tf']} + if model_config['children_mesh'] is not None: + self.resource_model[f"{resource_config['id']}_"] = { + 'mesh': f"{str(self.mesh_path)}/device_mesh/resources/{model_config['children_mesh']}", + 'mesh_tf': model_config['children_mesh_tf'] + } + resources = initialize_resources([resource_config]) + resource_dict = {item['id']: item for item in resources} + self.resource_config_dict = {**self.resource_config_dict,**resource_dict} + tf_dict = self.resource_mesh_setup(resource_dict) + self.resource_tf_dict = {**self.resource_tf_dict,**tf_dict} + self.publish_resource_tf() + self.add_resource_collision_meshes(tf_dict) + + + def resource_mesh_setup(self, resource_config_dict:dict): + """move_group初始化完成后的设置""" + self.get_logger().info('开始设置资源网格管理器') + #遍历resource_config中的资源配置,判断panent是否在resource_model中, + resource_tf_dict = {} + for resource_id, resource_config in resource_config_dict.items(): + + parent = resource_config['parent'] + parent_link = 'world' + if parent in self.resource_model: + parent_link = parent + elif parent is None and resource_id in self.resource_model: + + pass + elif parent is not None and resource_id in self.resource_model: + parent_link = f"{self.resource_config_dict[parent]['parent']}_{parent}_device_link".replace("None","") + + else: + + continue + # 提取位置信息并转换单位 + position = { + "x": float(resource_config['position']['x'])/1000, + "y": float(resource_config['position']['y'])/1000, + "z": float(resource_config['position']['z'])/1000 + } + + rotation_dict = { + "x": 0, + "y": 0, + "z": 0 + } + + if 'rotation' in resource_config['config']: + rotation_dict = resource_config['config']['rotation'] + + # 从欧拉角转换为四元数 + q = quaternion_from_euler( + float(rotation_dict['x']), + float(rotation_dict['y']), + float(rotation_dict['z']) + ) + + rotation = { + "x": q[0], + "y": q[1], + "z": q[2], + "w": q[3] + } + + # 更新资源TF字典 + resource_tf_dict[resource_id] = { + "parent": parent_link, + "position": position, + "rotation": rotation + } + + return resource_tf_dict + + + def publish_resource_tf(self): + """ + 发布资源之间的TF关系 + + 遍历self.resource_tf_dict中的每个元素,根据key,parent,以及position和rotation, + 发布key和parent之间的tf关系 + """ + + transforms = [] + + # 遍历资源TF字典 + resource_tf_dict = copy.deepcopy(self.resource_tf_dict) + for resource_id, tf_info in resource_tf_dict.items(): + parent = tf_info['parent'] + position = tf_info['position'] + rotation = tf_info['rotation'] + + # 创建静态变换消息 + + transform = TransformStamped() + transform.header.stamp = self.get_clock().now().to_msg() + transform.header.frame_id = parent + transform.child_frame_id = resource_id + + # 设置位置 + transform.transform.translation.x = float(position['x']) + transform.transform.translation.y = float(position['y']) + transform.transform.translation.z = float(position['z']) + + # 设置旋转 + transform.transform.rotation.x = rotation['x'] + transform.transform.rotation.y = rotation['y'] + transform.transform.rotation.z = rotation['z'] + transform.transform.rotation.w = rotation['w'] + + transforms.append(transform) + + # 一次性发布所有静态变换 + if transforms: + self.tf_broadcaster.sendTransform(transforms) + # self.check_resource_pose_changes() + # self.get_logger().info(f'已发布 {len(transforms)} 个资源TF关系') + + + def check_resource_pose_changes(self): + """ + 遍历资源TF字典,计算每个资源相对于world的变换, + 与旧的位姿比较,记录发生变化的资源,并更新旧位姿记录。 + + Returns: + dict: 包含发生位姿变化的资源ID及其新位姿 + """ + if not self.move_group_ready: + self.check_move_group_ready() + return + changed_poses = {} + resource_tf_dict = copy.deepcopy(self.resource_tf_dict) + for resource_id in resource_tf_dict.keys(): + try: + # 获取从resource_id到world的转换 + + transform = self.tf_buffer.lookup_transform( + "world", + resource_id, + rclpy.time.Time(seconds=0), + rclpy.duration.Duration(seconds=5) + ) + + # 提取当前位姿信息 + current_pose = { + "position": { + "x": transform.transform.translation.x, + "y": transform.transform.translation.y, + "z": transform.transform.translation.z + }, + "rotation": { + "x": transform.transform.rotation.x, + "y": transform.transform.rotation.y, + "z": transform.transform.rotation.z, + "w": transform.transform.rotation.w + } + } + + # 检查是否存在旧位姿记录 + if resource_id not in self.old_resource_pose: + # 如果没有旧记录,则认为是新资源,记录变化 + changed_poses[resource_id] = current_pose + self.old_resource_pose[resource_id] = current_pose + else: + # 比较当前位姿与旧位姿 + old_pose = self.old_resource_pose[resource_id] + if (not self._is_pose_equal(current_pose, old_pose)): + # 如果位姿发生变化,记录新位姿 + changed_poses[resource_id] = current_pose + self.old_resource_pose[resource_id] = current_pose + + except Exception as e: + self.get_logger().warning(f"获取资源 {resource_id} 的世界坐标变换失败: {e}") + + if changed_poses != {}: + self.zero_count = 0 + changed_poses_msg = String() + changed_poses_msg.data = json.dumps(changed_poses) + self.resource_pose_publisher.publish(changed_poses_msg) + else: + if self.zero_count > self.rate: + self.zero_count = 0 + changed_poses_msg = String() + changed_poses_msg.data = json.dumps(changed_poses) + self.resource_pose_publisher.publish(changed_poses_msg) + self.zero_count += 1 + + + + + def _is_pose_equal(self, pose1, pose2, tolerance=1e-7): + """ + 比较两个位姿是否相等(考虑浮点数精度) + + Args: + pose1: 第一个位姿 + pose2: 第二个位姿 + tolerance: 浮点数比较的容差 + + Returns: + bool: 如果位姿相等返回True,否则返回False + """ + # 比较位置 + pos1 = pose1["position"] + pos2 = pose2["position"] + if (abs(pos1["x"] - pos2["x"]) > tolerance or + abs(pos1["y"] - pos2["y"]) > tolerance or + abs(pos1["z"] - pos2["z"]) > tolerance): + return False + + # 比较旋转 + rot1 = pose1["rotation"] + rot2 = pose2["rotation"] + if (abs(rot1["x"] - rot2["x"]) > tolerance or + abs(rot1["y"] - rot2["y"]) > tolerance or + abs(rot1["z"] - rot2["z"]) > tolerance or + abs(rot1["w"] - rot2["w"]) > tolerance): + return False + + return True + + def tf_update(self, goal_handle : ServerGoalHandle): + tf_update_msg = goal_handle.request + + try: + cmd_dict = json.loads(tf_update_msg.command.replace("'",'"')) + self.__planning_scene = self._get_planning_scene_service.call( + GetPlanningScene.Request() + ).scene + + for resource_id, target_parent in cmd_dict.items(): + + # 获取从resource_id到target_parent的转换 + transform = self.tf_buffer.lookup_transform( + target_parent, + resource_id, + rclpy.time.Time(seconds=0) + ) + + # 提取转换中的位置和旋转信息 + position = { + "x": transform.transform.translation.x, + "y": transform.transform.translation.y, + "z": transform.transform.translation.z + } + + rotation = { + "x": transform.transform.rotation.x, + "y": transform.transform.rotation.y, + "z": transform.transform.rotation.z, + "w": transform.transform.rotation.w + } + + self.resource_tf_dict[resource_id] = { + "parent": target_parent, + "position": position, + "rotation": rotation + } + + # self.attach_collision_object(id=resource_id,link_name=target_parent) + collision_object = AttachedCollisionObject( + id=resource_id, + link_name=target_parent, + object=CollisionObject( + id=resource_id, + operation=CollisionObject.ADD + ) + ) + + self.__planning_scene.robot_state.attached_collision_objects.append(collision_object) + req = ApplyPlanningScene.Request() + req.scene = self.__planning_scene + self._apply_planning_scene_service.call_async(req) + self.publish_resource_tf() + + except Exception as e: + self.get_logger().error(f"更新资源TF字典失败: {e}") + goal_handle.abort() + return SendCmd.Result(success=False) + goal_handle.succeed() + return SendCmd.Result(success=True) + + + def add_resource_collision_meshes(self,resource_tf_dict:dict): + """ + 遍历资源配置字典,为每个在resource_model中有对应模型的资源添加碰撞网格 + + 该方法检查每个资源ID是否在self.resource_model中有对应的3D模型文件路径, + 如果有,则调用add_collision_mesh方法将其添加到碰撞环境中。 + """ + self.get_logger().info('开始添加资源碰撞网格') + + self.__planning_scene = self._get_planning_scene_service.call( + GetPlanningScene.Request() + ).scene + for resource_id, tf_info in resource_tf_dict.items(): + + if resource_id in self.resource_model: + # 获取位置信息 + + position = [ + float(self.resource_model[resource_id]['mesh_tf'][0]), + float(self.resource_model[resource_id]['mesh_tf'][1]), + float(self.resource_model[resource_id]['mesh_tf'][2]) + ] + + # 获取旋转信息并转换为四元数 + + q = quaternion_from_euler( + float(self.resource_model[resource_id]['mesh_tf'][3]), + float(self.resource_model[resource_id]['mesh_tf'][4]), + float(self.resource_model[resource_id]['mesh_tf'][5]) + ) + + # 添加碰撞网格 + collision_object = self.get_collision_mesh( + filepath=self.resource_model[resource_id]['mesh'], + id=resource_id, + position=position, + quat_xyzw=q, + frame_id=resource_id + ) + self.__planning_scene.world.collision_objects.append(collision_object) + elif f"{tf_info['parent']}_" in self.resource_model: + # 获取资源的父级框架ID + id_ = f"{tf_info['parent']}_" + + # 获取位置信息 + position = [ + float(self.resource_model[id_]['mesh_tf'][0]), + float(self.resource_model[id_]['mesh_tf'][1]), + float(self.resource_model[id_]['mesh_tf'][2]) + ] + + # 获取旋转信息并转换为四元数 + + q = quaternion_from_euler( + float(self.resource_model[id_]['mesh_tf'][3]), + float(self.resource_model[id_]['mesh_tf'][4]), + float(self.resource_model[id_]['mesh_tf'][5]) + ) + + # 添加碰撞网格 + collision_object = self.get_collision_mesh( + filepath=self.resource_model[id_]['mesh'], + id=resource_id, + position=position, + quat_xyzw=q, + frame_id=resource_id + ) + + self.__planning_scene.world.collision_objects.append(collision_object) + + req = ApplyPlanningScene.Request() + req.scene = self.__planning_scene + self._apply_planning_scene_service.call_async(req) + + + self.get_logger().info('资源碰撞网格添加完成') + + + def add_collision_primitive( + self, + id: str, + primitive_type: int, + dimensions: Tuple[float, float, float], + pose: Optional[Union[PoseStamped, Pose]] = None, + position: Optional[Union[Point, Tuple[float, float, float]]] = None, + quat_xyzw: Optional[ + Union[Quaternion, Tuple[float, float, float, float]] + ] = None, + frame_id: Optional[str] = None, + operation: int = CollisionObject.ADD, + ): + """ + Add collision object with a primitive geometry specified by its dimensions. + + `primitive_type` can be one of the following: + - `SolidPrimitive.BOX` + - `SolidPrimitive.SPHERE` + - `SolidPrimitive.CYLINDER` + - `SolidPrimitive.CONE` + """ + + if (pose is None) and (position is None or quat_xyzw is None): + raise ValueError( + "Either `pose` or `position` and `quat_xyzw` must be specified!" + ) + + if isinstance(pose, PoseStamped): + pose_stamped = pose + elif isinstance(pose, Pose): + pose_stamped = PoseStamped( + header=Header( + stamp=self.get_clock().now().to_msg(), + frame_id=( + frame_id if frame_id is not None else self.__base_link_name + ), + ), + pose=pose, + ) + else: + if not isinstance(position, Point): + position = Point( + x=float(position[0]), y=float(position[1]), z=float(position[2]) + ) + if not isinstance(quat_xyzw, Quaternion): + quat_xyzw = Quaternion( + x=float(quat_xyzw[0]), + y=float(quat_xyzw[1]), + z=float(quat_xyzw[2]), + w=float(quat_xyzw[3]), + ) + pose_stamped = PoseStamped( + header=Header( + stamp=self.get_clock().now().to_msg(), + frame_id=( + frame_id if frame_id is not None else self.__base_link_name + ), + ), + pose=Pose(position=position, orientation=quat_xyzw), + ) + + msg = CollisionObject( + header=pose_stamped.header, + id=id, + operation=operation, + pose=pose_stamped.pose, + ) + + msg.primitives.append( + SolidPrimitive(type=primitive_type, dimensions=dimensions) + ) + + self.__collision_object_publisher.publish(msg) + + def add_collision_box( + self, + id: str, + size: Tuple[float, float, float], + pose: Optional[Union[PoseStamped, Pose]] = None, + position: Optional[Union[Point, Tuple[float, float, float]]] = None, + quat_xyzw: Optional[ + Union[Quaternion, Tuple[float, float, float, float]] + ] = None, + frame_id: Optional[str] = None, + operation: int = CollisionObject.ADD, + ): + """ + Add collision object with a box geometry specified by its size. + """ + + assert len(size) == 3, "Invalid size of the box!" + + self.add_collision_primitive( + id=id, + primitive_type=SolidPrimitive.BOX, + dimensions=size, + pose=pose, + position=position, + quat_xyzw=quat_xyzw, + frame_id=frame_id, + operation=operation, + ) + + def add_collision_sphere( + self, + id: str, + radius: float, + pose: Optional[Union[PoseStamped, Pose]] = None, + position: Optional[Union[Point, Tuple[float, float, float]]] = None, + quat_xyzw: Optional[ + Union[Quaternion, Tuple[float, float, float, float]] + ] = None, + frame_id: Optional[str] = None, + operation: int = CollisionObject.ADD, + ): + """ + Add collision object with a sphere geometry specified by its radius. + """ + + if quat_xyzw is None: + quat_xyzw = Quaternion(x=0.0, y=0.0, z=0.0, w=1.0) + + self.add_collision_primitive( + id=id, + primitive_type=SolidPrimitive.SPHERE, + dimensions=[ + radius, + ], + pose=pose, + position=position, + quat_xyzw=quat_xyzw, + frame_id=frame_id, + operation=operation, + ) + + def add_collision_cylinder( + self, + id: str, + height: float, + radius: float, + pose: Optional[Union[PoseStamped, Pose]] = None, + position: Optional[Union[Point, Tuple[float, float, float]]] = None, + quat_xyzw: Optional[ + Union[Quaternion, Tuple[float, float, float, float]] + ] = None, + frame_id: Optional[str] = None, + operation: int = CollisionObject.ADD, + ): + """ + Add collision object with a cylinder geometry specified by its height and radius. + """ + + self.add_collision_primitive( + id=id, + primitive_type=SolidPrimitive.CYLINDER, + dimensions=[height, radius], + pose=pose, + position=position, + quat_xyzw=quat_xyzw, + frame_id=frame_id, + operation=operation, + ) + + def add_collision_cone( + self, + id: str, + height: float, + radius: float, + pose: Optional[Union[PoseStamped, Pose]] = None, + position: Optional[Union[Point, Tuple[float, float, float]]] = None, + quat_xyzw: Optional[ + Union[Quaternion, Tuple[float, float, float, float]] + ] = None, + frame_id: Optional[str] = None, + operation: int = CollisionObject.ADD, + ): + """ + Add collision object with a cone geometry specified by its height and radius. + """ + + self.add_collision_primitive( + id=id, + primitive_type=SolidPrimitive.CONE, + dimensions=[height, radius], + pose=pose, + position=position, + quat_xyzw=quat_xyzw, + frame_id=frame_id, + operation=operation, + ) + + def get_collision_mesh( + self, + filepath: Optional[str], + id: str, + pose: Optional[Union[PoseStamped, Pose]] = None, + position: Optional[Union[Point, Tuple[float, float, float]]] = None, + quat_xyzw: Optional[ + Union[Quaternion, Tuple[float, float, float, float]] + ] = None, + frame_id: Optional[str] = None, + operation: int = CollisionObject.ADD, + scale: Union[float, Tuple[float, float, float]] = 1.0, + mesh: Optional[Any] = None, + ): + """ + Add collision object with a mesh geometry. Either `filepath` must be + specified or `mesh` must be provided. + Note: This function required 'trimesh' Python module to be installed. + """ + + # Load the mesh + try: + import trimesh + except ImportError as err: + raise ImportError( + "Python module 'trimesh' not found! Please install it manually in order " + "to add collision objects into the MoveIt 2 planning scene." + ) from err + + # Check the parameters + if (pose is None) and (position is None or quat_xyzw is None): + raise ValueError( + "Either `pose` or `position` and `quat_xyzw` must be specified!" + ) + if (filepath is None and mesh is None) or ( + filepath is not None and mesh is not None + ): + raise ValueError("Exactly one of `filepath` or `mesh` must be specified!") + if mesh is not None and not isinstance(mesh, trimesh.Trimesh): + raise ValueError("`mesh` must be an instance of `trimesh.Trimesh`!") + + if isinstance(pose, PoseStamped): + pose_stamped = pose + elif isinstance(pose, Pose): + pose_stamped = PoseStamped( + header=Header( + stamp=self.get_clock().now().to_msg(), + frame_id=( + frame_id if frame_id is not None else self.__base_link_name + ), + ), + pose=pose, + ) + else: + if not isinstance(position, Point): + position = Point( + x=float(position[0]), y=float(position[1]), z=float(position[2]) + ) + if not isinstance(quat_xyzw, Quaternion): + quat_xyzw = Quaternion( + x=float(quat_xyzw[0]), + y=float(quat_xyzw[1]), + z=float(quat_xyzw[2]), + w=float(quat_xyzw[3]), + ) + pose_stamped = PoseStamped( + header=Header( + stamp=self.get_clock().now().to_msg(), + frame_id=( + frame_id if frame_id is not None else self.__base_link_name + ), + ), + pose=Pose(position=position, orientation=quat_xyzw), + ) + + msg = CollisionObject( + header=pose_stamped.header, + id=id, + operation=operation, + pose=pose_stamped.pose, + ) + + if filepath is not None: + mesh = trimesh.load(filepath) + + # Scale the mesh + if isinstance(scale, float): + scale = (scale, scale, scale) + if not (scale[0] == scale[1] == scale[2] == 1.0): + # If the mesh was passed in as a parameter, make a copy of it to + # avoid transforming the original. + if filepath is not None: + mesh = mesh.copy() + # Transform the mesh + transform = np.eye(4) + np.fill_diagonal(transform, scale) + mesh.apply_transform(transform) + + msg.meshes.append( + Mesh( + triangles=[MeshTriangle(vertex_indices=face) for face in mesh.faces], + vertices=[ + Point(x=vert[0], y=vert[1], z=vert[2]) for vert in mesh.vertices + ], + ) + ) + + # self.__collision_object_publisher.publish(msg) + return msg + + def add_collision_mesh( + self, + filepath: Optional[str], + id: str, + pose: Optional[Union[PoseStamped, Pose]] = None, + position: Optional[Union[Point, Tuple[float, float, float]]] = None, + quat_xyzw: Optional[ + Union[Quaternion, Tuple[float, float, float, float]] + ] = None, + frame_id: Optional[str] = None, + operation: int = CollisionObject.ADD, + scale: Union[float, Tuple[float, float, float]] = 1.0, + mesh: Optional[Any] = None, + ): + """ + Add collision object with a mesh geometry. Either `filepath` must be + specified or `mesh` must be provided. + Note: This function required 'trimesh' Python module to be installed. + """ + + # Load the mesh + try: + import trimesh + except ImportError as err: + raise ImportError( + "Python module 'trimesh' not found! Please install it manually in order " + "to add collision objects into the MoveIt 2 planning scene." + ) from err + + # Check the parameters + if (pose is None) and (position is None or quat_xyzw is None): + raise ValueError( + "Either `pose` or `position` and `quat_xyzw` must be specified!" + ) + if (filepath is None and mesh is None) or ( + filepath is not None and mesh is not None + ): + raise ValueError("Exactly one of `filepath` or `mesh` must be specified!") + if mesh is not None and not isinstance(mesh, trimesh.Trimesh): + raise ValueError("`mesh` must be an instance of `trimesh.Trimesh`!") + + if isinstance(pose, PoseStamped): + pose_stamped = pose + elif isinstance(pose, Pose): + pose_stamped = PoseStamped( + header=Header( + stamp=self.get_clock().now().to_msg(), + frame_id=( + frame_id if frame_id is not None else self.__base_link_name + ), + ), + pose=pose, + ) + else: + if not isinstance(position, Point): + position = Point( + x=float(position[0]), y=float(position[1]), z=float(position[2]) + ) + if not isinstance(quat_xyzw, Quaternion): + quat_xyzw = Quaternion( + x=float(quat_xyzw[0]), + y=float(quat_xyzw[1]), + z=float(quat_xyzw[2]), + w=float(quat_xyzw[3]), + ) + pose_stamped = PoseStamped( + header=Header( + stamp=self.get_clock().now().to_msg(), + frame_id=( + frame_id if frame_id is not None else self.__base_link_name + ), + ), + pose=Pose(position=position, orientation=quat_xyzw), + ) + + msg = CollisionObject( + header=pose_stamped.header, + id=id, + operation=operation, + pose=pose_stamped.pose, + ) + + if filepath is not None: + mesh = trimesh.load(filepath) + + # Scale the mesh + if isinstance(scale, float): + scale = (scale, scale, scale) + if not (scale[0] == scale[1] == scale[2] == 1.0): + # If the mesh was passed in as a parameter, make a copy of it to + # avoid transforming the original. + if filepath is not None: + mesh = mesh.copy() + # Transform the mesh + transform = np.eye(4) + np.fill_diagonal(transform, scale) + mesh.apply_transform(transform) + + msg.meshes.append( + Mesh( + triangles=[MeshTriangle(vertex_indices=face) for face in mesh.faces], + vertices=[ + Point(x=vert[0], y=vert[1], z=vert[2]) for vert in mesh.vertices + ], + ) + ) + + self.__collision_object_publisher.publish(msg) + + def remove_collision_object(self, id: str): + """ + Remove collision object specified by its `id`. + """ + + msg = CollisionObject() + msg.id = id + msg.operation = CollisionObject.REMOVE + msg.header.stamp = self.get_clock().now().to_msg() + self.__collision_object_publisher.publish(msg) + + def remove_collision_mesh(self, id: str): + """ + Remove collision mesh specified by its `id`. + Identical to `remove_collision_object()`. + """ + + self.remove_collision_object(id) + + def attach_collision_object( + self, + id: str, + link_name: Optional[str] = None, + touch_links: List[str] = [], + weight: float = 0.0, + ): + """ + Attach collision object to the robot. + """ + + if link_name is None: + link_name = self.__end_effector_name + + msg = AttachedCollisionObject( + object=CollisionObject(id=id, operation=CollisionObject.ADD) + ) + msg.link_name = link_name + msg.touch_links = touch_links + msg.weight = weight + + self.__attached_collision_object_publisher.publish(msg) + + def detach_collision_object(self, id: int): + """ + Detach collision object from the robot. + """ + + msg = AttachedCollisionObject( + object=CollisionObject(id=id, operation=CollisionObject.REMOVE) + ) + self.__attached_collision_object_publisher.publish(msg) + + def detach_all_collision_objects(self): + """ + Detach collision object from the robot. + """ + + msg = AttachedCollisionObject( + object=CollisionObject(operation=CollisionObject.REMOVE) + ) + self.__attached_collision_object_publisher.publish(msg) + + def move_collision( + self, + id: str, + position: Union[Point, Tuple[float, float, float]], + quat_xyzw: Union[Quaternion, Tuple[float, float, float, float]], + frame_id: Optional[str] = None, + ): + """ + Move collision object specified by its `id`. + """ + + msg = CollisionObject() + + if not isinstance(position, Point): + position = Point( + x=float(position[0]), y=float(position[1]), z=float(position[2]) + ) + if not isinstance(quat_xyzw, Quaternion): + quat_xyzw = Quaternion( + x=float(quat_xyzw[0]), + y=float(quat_xyzw[1]), + z=float(quat_xyzw[2]), + w=float(quat_xyzw[3]), + ) + + pose = Pose() + pose.position = position + pose.orientation = quat_xyzw + msg.pose = pose + msg.id = id + msg.operation = CollisionObject.MOVE + msg.header.frame_id = ( + frame_id if frame_id is not None else self.__base_link_name + ) + msg.header.stamp = self.get_clock().now().to_msg() + + self.__collision_object_publisher.publish(msg) + + def update_planning_scene(self) -> bool: + """ + Gets the current planning scene. Returns whether the service call was + successful. + """ + + if not self._get_planning_scene_service.service_is_ready(): + self.get_logger().warn( + f"Service '{self._get_planning_scene_service.srv_name}' is not yet available. Better luck next time!" + ) + return False + self.__planning_scene = self._get_planning_scene_service.call( + GetPlanningScene.Request() + ).scene + return True + + def allow_collisions(self, id: str, allow: bool) -> Optional[Future]: + """ + Takes in the ID of an element in the planning scene. Modifies the allowed + collision matrix to (dis)allow collisions between that object and all other + object. + + If `allow` is True, a plan will succeed even if the robot collides with that object. + If `allow` is False, a plan will fail if the robot collides with that object. + Returns whether it successfully updated the allowed collision matrix. + + Returns the future of the service call. + """ + # Update the planning scene + if not self.update_planning_scene(): + return None + allowed_collision_matrix = self.__planning_scene.allowed_collision_matrix + self.__old_allowed_collision_matrix = copy.deepcopy(allowed_collision_matrix) + + # Get the location in the allowed collision matrix of the object + j = None + if id not in allowed_collision_matrix.entry_names: + allowed_collision_matrix.entry_names.append(id) + else: + j = allowed_collision_matrix.entry_names.index(id) + # For all other objects, (dis)allow collisions with the object with `id` + for i in range(len(allowed_collision_matrix.entry_values)): + if j is None: + allowed_collision_matrix.entry_values[i].enabled.append(allow) + elif i != j: + allowed_collision_matrix.entry_values[i].enabled[j] = allow + # For the object with `id`, (dis)allow collisions with all other objects + allowed_collision_entry = AllowedCollisionEntry( + enabled=[allow for _ in range(len(allowed_collision_matrix.entry_names))] + ) + if j is None: + allowed_collision_matrix.entry_values.append(allowed_collision_entry) + else: + allowed_collision_matrix.entry_values[j] = allowed_collision_entry + + # Apply the new planning scene + if not self._apply_planning_scene_service.service_is_ready(): + self.get_logger().warn( + f"Service '{self._apply_planning_scene_service.srv_name}' is not yet available. Better luck next time!" + ) + return None + return self._apply_planning_scene_service.call_async( + ApplyPlanningScene.Request(scene=self.__planning_scene) + ) + + def process_allow_collision_future(self, future: Future) -> bool: + """ + Return whether the allow collision service call is done and has succeeded + or not. If it failed, reset the allowed collision matrix to the old one. + """ + if not future.done(): + return False + + # Get response + resp = future.result() + + # If it failed, restore the old planning scene + if not resp.success: + self.__planning_scene.allowed_collision_matrix = ( + self.__old_allowed_collision_matrix + ) + + return resp.success + + def clear_all_collision_objects(self) -> Optional[Future]: + """ + Removes all attached and un-attached collision objects from the planning scene. + + Returns a future for the ApplyPlanningScene service call. + """ + # Update the planning scene + if not self.update_planning_scene(): + return None + self.__old_planning_scene = copy.deepcopy(self.__planning_scene) + + # Remove all collision objects from the planning scene + self.__planning_scene.world.collision_objects = [] + self.__planning_scene.robot_state.attached_collision_objects = [] + + # Apply the new planning scene + if not self._apply_planning_scene_service.service_is_ready(): + self.get_logger().warn( + f"Service '{self._apply_planning_scene_service.srv_name}' is not yet available. Better luck next time!" + ) + return None + return self._apply_planning_scene_service.call_async( + ApplyPlanningScene.Request(scene=self.__planning_scene) + ) diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index c0886cd4..04b54373 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -1,7 +1,7 @@ from unilabos.utils.log import logger -class DeviceNodeResourceTracker: +class DeviceNodeResourceTracker(object): def __init__(self): self.resources = [] @@ -15,43 +15,46 @@ class DeviceNodeResourceTracker: return resource def add_resource(self, resource): - # 使用内存地址跟踪是否为同一个resource for r in self.resources: if id(r) == id(resource): return - # 添加资源到跟踪器 self.resources.append(resource) def clear_resource(self): self.resources = [] - def figure_resource(self, resource): - # 使用内存地址跟踪是否为同一个resource - if isinstance(resource, list): - return [self.figure_resource(r) for r in resource] - res_id = resource.id if hasattr(resource, "id") else None - res_name = resource.name if hasattr(resource, "name") else None + def figure_resource(self, query_resource): + if isinstance(query_resource, list): + return [self.figure_resource(r) for r in query_resource] + res_id = query_resource.id if hasattr(query_resource, "id") else (query_resource.get("id") if isinstance(query_resource, dict) else None) + res_name = query_resource.name if hasattr(query_resource, "name") else (query_resource.get("name") if isinstance(query_resource, dict) else None) res_identifier = res_id if res_id else res_name identifier_key = "id" if res_id else "name" - resource_cls_type = type(resource) + resource_cls_type = type(query_resource) if res_identifier is None: - logger.warning(f"resource {resource} 没有id或name,暂不能对应figure") + logger.warning(f"resource {query_resource} 没有id或name,暂不能对应figure") res_list = [] for r in self.resources: - res_list.extend( - self.loop_find_resource(r, resource_cls_type, identifier_key, getattr(resource, identifier_key)) - ) - assert len(res_list) == 1, f"找到多个资源,请检查资源是否唯一: {res_list}" - self.root_resource2resource[id(resource)] = res_list[0] + if isinstance(query_resource, dict): + res_list.extend( + self.loop_find_resource(r, resource_cls_type, identifier_key, query_resource[identifier_key]) + ) + else: + res_list.extend( + self.loop_find_resource(r, resource_cls_type, identifier_key, getattr(query_resource, identifier_key)) + ) + assert len(res_list) == 1, f"{query_resource} 找到多个资源,请检查资源是否唯一: {res_list}" + self.root_resource2resource[id(query_resource)] = res_list[0] # 后续加入其他对比方式 return res_list[0] - def loop_find_resource(self, resource, resource_cls_type, identifier_key, compare_value): + def loop_find_resource(self, resource, target_resource_cls_type, identifier_key, compare_value): res_list = [] + # print(resource, target_resource_cls_type, identifier_key, compare_value) children = getattr(resource, "children", []) for child in children: - res_list.extend(self.loop_find_resource(child, resource_cls_type, identifier_key, compare_value)) - if resource_cls_type == type(resource): + res_list.extend(self.loop_find_resource(child, target_resource_cls_type, identifier_key, compare_value)) + if target_resource_cls_type == type(resource) or target_resource_cls_type == dict: if hasattr(resource, identifier_key): if getattr(resource, identifier_key) == compare_value: res_list.append(resource) diff --git a/unilabos/ros/utils/driver_creator.py b/unilabos/ros/utils/driver_creator.py index 2ea30856..1218725e 100644 --- a/unilabos/ros/utils/driver_creator.py +++ b/unilabos/ros/utils/driver_creator.py @@ -6,6 +6,7 @@ """ import asyncio import inspect +import json import traceback from abc import abstractmethod from typing import Type, Any, Dict, Optional, TypeVar, Generic @@ -218,12 +219,22 @@ class PyLabRobotCreator(DeviceClassCreator[T]): logger.error(f"PyLabRobot反序列化失败: {deserialize_error}") logger.error(f"PyLabRobot反序列化堆栈: {stack}") - return self.device_instance + return self.device_instance def post_create(self): if hasattr(self.device_instance, "setup") and asyncio.iscoroutinefunction(getattr(self.device_instance, "setup")): from unilabos.ros.nodes.base_device_node import ROS2DeviceNode - ROS2DeviceNode.run_async_func(getattr(self.device_instance, "setup")).add_done_callback(lambda x: logger.debug(f"PyLabRobot设备实例 {self.device_instance} 设置完成")) + def done_cb(*args): + logger.debug(f"PyLabRobot设备实例 {self.device_instance} 设置完成") + from unilabos.config.config import BasicConfig + if BasicConfig.vis_2d_enable: + from pylabrobot.visualizer.visualizer import Visualizer + vis = Visualizer(resource=self.device_instance, open_browser=True) + def vis_done_cb(*args): + logger.info(f"PyLabRobot设备实例开启了Visualizer {self.device_instance}") + ROS2DeviceNode.run_async_func(vis.setup).add_done_callback(vis_done_cb) + logger.debug(f"PyLabRobot设备实例提交开启Visualizer {self.device_instance}") + ROS2DeviceNode.run_async_func(getattr(self.device_instance, "setup")).add_done_callback(done_cb) class ProtocolNodeCreator(DeviceClassCreator[T]): diff --git a/unilabos_msgs/CMakeLists.txt b/unilabos_msgs/CMakeLists.txt index acaad771..69fbaa3a 100644 --- a/unilabos_msgs/CMakeLists.txt +++ b/unilabos_msgs/CMakeLists.txt @@ -43,6 +43,25 @@ set(action_files "action/LiquidHandlerStamp.action" "action/LiquidHandlerTransfer.action" + "action/DPLiquidHandlerAddLiquid.action" + "action/DPLiquidHandlerCustomDelay.action" + "action/DPLiquidHandlerMix.action" + "action/DPLiquidHandlerMoveTo.action" + "action/DPLiquidHandlerRemoveLiquid.action" + "action/DPLiquidHandlerSetTiprack.action" + "action/DPLiquidHandlerTouchTip.action" + "action/DPLiquidHandlerTransferLiquid.action" + + "action/EmptyIn.action" + "action/FloatSingleInput.action" + "action/IntSingleInput.action" + "action/StrSingleInput.action" + "action/Point3DSeparateInput.action" + + "action/ResourceCreateFromOuter.action" + + "action/SolidDispenseAddPowderTube.action" + "action/PumpTransfer.action" "action/Clean.action" "action/Separate.action" diff --git a/unilabos_msgs/action/DPLiquidHandlerAddLiquid.action b/unilabos_msgs/action/DPLiquidHandlerAddLiquid.action new file mode 100644 index 00000000..0611b276 --- /dev/null +++ b/unilabos_msgs/action/DPLiquidHandlerAddLiquid.action @@ -0,0 +1,20 @@ +float64[] asp_vols +float64[] dis_vols +Resource[] reagent_sources +Resource[] targets +int32[] use_channels +float64[] flow_rates +geometry_msgs/Point[] offsets +float64[] liquid_height +float64[] blow_out_air_volume +string spread +bool is_96_well +int32 mix_time +int32 mix_vol +int32 mix_rate +float64 mix_liquid_height +string[] none_keys +--- +bool success +--- +# 反馈 \ No newline at end of file diff --git a/unilabos_msgs/action/DPLiquidHandlerCustomDelay.action b/unilabos_msgs/action/DPLiquidHandlerCustomDelay.action new file mode 100644 index 00000000..29f9b45b --- /dev/null +++ b/unilabos_msgs/action/DPLiquidHandlerCustomDelay.action @@ -0,0 +1,6 @@ +float64 seconds +string msg +--- +bool success +--- +# 反馈 \ No newline at end of file diff --git a/unilabos_msgs/action/DPLiquidHandlerMix.action b/unilabos_msgs/action/DPLiquidHandlerMix.action new file mode 100644 index 00000000..81d1b71c --- /dev/null +++ b/unilabos_msgs/action/DPLiquidHandlerMix.action @@ -0,0 +1,11 @@ +Resource[] targets +int32 mix_time +int32 mix_vol +float64 height_to_bottom +geometry_msgs/Point[] offsets +float64 mix_rate +string[] none_keys +--- +bool success +--- +# 反馈 \ No newline at end of file diff --git a/unilabos_msgs/action/DPLiquidHandlerMoveTo.action b/unilabos_msgs/action/DPLiquidHandlerMoveTo.action new file mode 100644 index 00000000..740d0fc6 --- /dev/null +++ b/unilabos_msgs/action/DPLiquidHandlerMoveTo.action @@ -0,0 +1,7 @@ +Resource well +float64 dis_to_top +int32 channel +--- +bool success +--- +# 反馈 \ No newline at end of file diff --git a/unilabos_msgs/action/DPLiquidHandlerRemoveLiquid.action b/unilabos_msgs/action/DPLiquidHandlerRemoveLiquid.action new file mode 100644 index 00000000..e6b43c53 --- /dev/null +++ b/unilabos_msgs/action/DPLiquidHandlerRemoveLiquid.action @@ -0,0 +1,17 @@ +float64[] vols +Resource[] sources +Resource waste_liquid +int32[] use_channels +float64[] flow_rates +geometry_msgs/Point[] offsets +float64[] liquid_height +float64[] blow_out_air_volume +string spread +int32[] delays +bool is_96_well +float64[] top +string[] none_keys +--- +bool success +--- +# 反馈 \ No newline at end of file diff --git a/unilabos_msgs/action/DPLiquidHandlerSetTiprack.action b/unilabos_msgs/action/DPLiquidHandlerSetTiprack.action new file mode 100644 index 00000000..437d3e3f --- /dev/null +++ b/unilabos_msgs/action/DPLiquidHandlerSetTiprack.action @@ -0,0 +1,5 @@ +Resource[] tip_racks +--- +bool success +--- +# 反馈 \ No newline at end of file diff --git a/unilabos_msgs/action/DPLiquidHandlerTouchTip.action b/unilabos_msgs/action/DPLiquidHandlerTouchTip.action new file mode 100644 index 00000000..e0c31046 --- /dev/null +++ b/unilabos_msgs/action/DPLiquidHandlerTouchTip.action @@ -0,0 +1,5 @@ +Resource[] targets +--- +bool success +--- +# 反馈 \ No newline at end of file diff --git a/unilabos_msgs/action/DPLiquidHandlerTransferLiquid.action b/unilabos_msgs/action/DPLiquidHandlerTransferLiquid.action new file mode 100644 index 00000000..39df59bb --- /dev/null +++ b/unilabos_msgs/action/DPLiquidHandlerTransferLiquid.action @@ -0,0 +1,25 @@ +float64[] asp_vols +float64[] dis_vols +Resource[] sources +Resource[] targets +Resource[] tip_racks +int32[] use_channels +float64[] asp_flow_rates +float64[] dis_flow_rates +geometry_msgs/Point[] offsets +bool touch_tip +float64[] liquid_height +float64[] blow_out_air_volume +string spread +bool is_96_well +string mix_stage +int32[] mix_times +int32 mix_vol +int32 mix_rate +float64 mix_liquid_height +int32[] delays +string[] none_keys +--- +bool success +--- +# 反馈 \ No newline at end of file diff --git a/unilabos_msgs/action/LiquidHandlerAspirate.action b/unilabos_msgs/action/LiquidHandlerAspirate.action index 3784d943..f03ad07a 100644 --- a/unilabos_msgs/action/LiquidHandlerAspirate.action +++ b/unilabos_msgs/action/LiquidHandlerAspirate.action @@ -1,12 +1,11 @@ -# Bio Resource[] resources float64[] vols int32[] use_channels float64[] flow_rates -float64 end_delay geometry_msgs/Point[] offsets float64[] liquid_height float64[] blow_out_air_volume +string spread="wide" --- bool success --- \ No newline at end of file diff --git a/unilabos_msgs/action/LiquidHandlerDispense.action b/unilabos_msgs/action/LiquidHandlerDispense.action index f934aec2..ba5360ae 100644 --- a/unilabos_msgs/action/LiquidHandlerDispense.action +++ b/unilabos_msgs/action/LiquidHandlerDispense.action @@ -1,4 +1,3 @@ -# Bio # 请求字段 Resource[] resources float64[] vols @@ -6,7 +5,7 @@ int32[] use_channels float64[] flow_rates geometry_msgs/Point[] offsets int32[] blow_out_air_volume -string spread +string spread="wide" --- # 结果字段 bool success diff --git a/unilabos_msgs/action/ResourceCreateFromOuter.action b/unilabos_msgs/action/ResourceCreateFromOuter.action new file mode 100644 index 00000000..e0eeb1c7 --- /dev/null +++ b/unilabos_msgs/action/ResourceCreateFromOuter.action @@ -0,0 +1,8 @@ +Resource[] resources +string[] device_ids +string[] bind_parent_ids +geometry_msgs/Point[] bind_locations +string[] other_calling_params +--- +bool success +--- \ No newline at end of file