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