Device visualization (#14)

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* add 3d visualization

* 完成在main中启动设备可视化

完成在main中启动设备可视化,并输出物料ID:mesh的对应关系resource_model

添加物料模型管理类,遍历物料与resource_model,完成TF数据收集

* 完成TF发布

* 修改模型方向,在yaml中添加变换属性

* 添加物料tf变化时,发送topic到前端

另外修改了物料初始化的方法,防止在tf还未发布时提前建立物料模型与发布话题

* 添加关节发布节点与物料可视化节点进入unilab

* 使用json启动plr与3D模型仿真

* 完成启动OT并联动rviz

* 修复rviz位置问题,

修复rviz位置问题,
在无tf变动时减缓发送频率
在backend中添加物料跟随方法

* fix: running logic

* fix: running logic

* fix: missing ot

* 在main中直接初始化republisher和物料的mesh节点

* 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中

---------

Co-authored-by: zhangshixiang <@zhangshixiang>
Co-authored-by: wznln <18435084+Xuwznln@users.noreply.github.com>
This commit is contained in:
q434343
2025-05-06 09:37:57 +08:00
committed by GitHub
parent 752442cb37
commit a52133b7d0
53 changed files with 2654 additions and 25 deletions

View File

@@ -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": [

View File

@@ -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": 10
},
"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": [
]
}

View File

@@ -12,6 +12,8 @@ def start_backend(
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.")

View File

@@ -1,10 +1,17 @@
import argparse
import asyncio
import os
import signal
import sys
import json
import time
import yaml
from copy import deepcopy
import threading
import rclpy
from unilabos.ros.nodes.resource_tracker import DeviceNodeResourceTracker
# 首先添加项目根目录到路径
current_dir = os.path.dirname(os.path.abspath(__file__))
@@ -14,6 +21,10 @@ if ilabos_dir not in sys.path:
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
from unilabos.ros.nodes.presets.joint_republisher import JointRepublisher
from unilabos.ros.nodes.presets.resource_mesh_manager import ResourceMeshManager
from rclpy.executors import MultiThreadedExecutor
def parse_args():
@@ -70,7 +81,12 @@ def parse_args():
default=True,
help="是否在启动时打开信息页",
)
parser.add_argument(
"--visual",
choices=["rviz", "web","None"],
default="rviz",
help="选择可视化工具: 'rviz''web''None',默认'rviz'",
)
return parser.parse_args()
@@ -121,6 +137,7 @@ def main():
# 注册表
build_registry(args_dict["registry_path"])
if args_dict["graph"] is not None:
import unilabos.resources.graphio as graph_res
graph_res.physical_setup_graph = (
@@ -132,6 +149,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 +184,28 @@ def main():
signal.signal(signal.SIGINT, _exit)
signal.signal(signal.SIGTERM, _exit)
mqtt_client.start()
args_dict["resources_mesh_config"] = {}
if args_dict["visual"] != "None":
if args_dict["visual"] == "rviz":
enable_rviz=True
elif args_dict["visual"] == "web":
enable_rviz=False
resource_visualization = ResourceVisualization(devices_and_resources, args_dict["resources_config"] ,enable_rviz=enable_rviz)
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"] = resource_visualization.resource_model
# 将joint_republisher和resource_mesh_manager添加进 main_slave_run.py中
start_backend(**args_dict)
server_thread = threading.Thread(target=start_server)
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()
if __name__ == "__main__":

View File

@@ -35,7 +35,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))

View File

@@ -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"
}
}

View File

@@ -0,0 +1,210 @@
<?xml version="1.0" ?>
<robot xmlns:xacro="http://ros.org/wiki/xacro">
<xacro:macro name="opentrons_liquid_handler"
params="parent_link:='' station_name:='' device_name:='' x:=0 y:=0 z:=0 r:=0 mesh_path:=''">
<joint name="${station_name}${device_name}base_link_joint" type="fixed">
<origin xyz="${x} ${y} ${z}" rpy="0 0 ${r}" />
<parent link="${parent_link}"/>
<child link="${station_name}${device_name}device_link"/>
<axis xyz="0 0 0"/>
</joint>
<link name="${station_name}${device_name}device_link"/>
<joint name="${station_name}${device_name}device_link_joint" type="fixed">
<origin xyz="-0.11565 0.496 0" rpy="0 0 0" />
<parent link="${station_name}${device_name}device_link"/>
<child link="${station_name}${device_name}main_link"/>
<axis xyz="0 0 0"/>
</joint>
<link name='${station_name}${device_name}main_link'>
<visual>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-0.stl"/>
</geometry>
<material name="">
<color rgba="0.756862745098039 0.768627450980392 0.752941176470588 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-0.stl"/>
</geometry>
</collision>
</link>
<link name='${station_name}${device_name}first_link'>
<visual>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-1.stl"/>
</geometry>
<material name="">
<color rgba="0.756862745098039 0.768627450980392 0.752941176470588 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-1.stl"/>
</geometry>
</collision>
</link>
<link name='${station_name}${device_name}second_link'>
<visual>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-2.stl"/>
</geometry>
<material name="">
<color rgba="0.756862745098039 0.768627450980392 0.752941176470588 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-2.stl"/>
</geometry>
</collision>
</link>
<link name='${station_name}${device_name}third_link'>
<visual>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-3a.stl"/>
</geometry>
<material name="">
<color rgba="0.756862745098039 0.768627450980392 0.752941176470588 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-3a.stl"/>
</geometry>
</collision>
</link>
<link name='${station_name}${device_name}fourth_link'>
<visual>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-3b.stl"/>
</geometry>
<material name="">
<color rgba="0.756862745098039 0.768627450980392 0.752941176470588 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/opentrons_liquid_handler/meshes/ot2-3b.stl"/>
</geometry>
</collision>
</link>
<joint name='${station_name}${device_name}first_joint' type='prismatic'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}first_link"/>
<origin rpy="0 0 0" xyz="0 0 0"/>
<axis xyz="0 -1 0"/>
<limit effort="-1" lower="-0.2" upper="0.13" velocity="-1"/>
<dynamics damping="0.1"/>
</joint>
<joint name='${station_name}${device_name}second_joint' type='prismatic'>
<parent link="${station_name}${device_name}first_link"/>
<child link="${station_name}${device_name}second_link"/>
<origin rpy="0 0 0" xyz="0 0 0"/>
<axis xyz="-1 0 0"/>
<limit effort="-1" lower="-0.15" upper="0.15" velocity="-1"/>
<dynamics damping="0.1"/>
</joint>
<joint name='${station_name}${device_name}third_joint' type='prismatic'>
<parent link="${station_name}${device_name}second_link"/>
<child link="${station_name}${device_name}third_link"/>
<origin rpy="0 0 0" xyz="0 0 0"/>
<axis xyz="0 0 -1"/>
<limit effort="-1" lower="0" upper="0.22" velocity="-1"/>
<dynamics damping="0.1"/>
</joint>
<joint name='${station_name}${device_name}fourth_joint' type='prismatic'>
<parent link="${station_name}${device_name}second_link"/>
<child link="${station_name}${device_name}fourth_link"/>
<origin rpy="0 0 0" xyz="0 0 0"/>
<axis xyz="0 0 -1"/>
<limit effort="-1" lower="0" upper="0.22" velocity="-1"/>
<dynamics damping="0.1"/>
</joint>
<link name='${station_name}${device_name}socketTypeGenericSbsFootprint'/>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_10_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.1795 -0.1825 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_7_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.1795 -0.273 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_4_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.1795 -0.3635 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_1_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.1795 -0.454 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_11_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.312 -0.1825 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_8_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.312 -0.273 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_5_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.312 -0.3635 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_2_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.312 -0.454 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_9_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.4445 -0.273 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_6_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.4445 -0.3635 0.07"/>
</joint>
<joint name='${station_name}${device_name}socketTypeGenericSbsFootprint_3_60_1' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeGenericSbsFootprint"/>
<origin rpy="0 0 -1.57" xyz="0.4445 -0.454 0.07"/>
</joint>
<link name='${station_name}${device_name}socketTypeHEPAModule'/>
<joint name='${station_name}${device_name}socketTypeHEPAModule' type='fixed'>
<parent link="${station_name}${device_name}main_link"/>
<child link="${station_name}${device_name}socketTypeHEPAModule"/>
<origin rpy="0 0 0" xyz="0.31 -0.26 0.66"/>
</joint>
</xacro:macro>
</robot>

View File

@@ -0,0 +1,10 @@
{
"private_param":
{
},
"public_param":
{
}
}

View File

@@ -0,0 +1,6 @@
{
"slider_joint": {
"child":"slider",
"axis" : "x"
}
}

View File

@@ -0,0 +1,136 @@
<?xml version="1.0" ?>
<!-- =================================================================================== -->
<!-- | This document was autogenerated by xacro from slide.urdf | -->
<!-- | EDITING THIS FILE BY HAND IS NOT RECOMMENDED | -->
<!-- =================================================================================== -->
<!-- This URDF was automatically created by SolidWorks to URDF Exporter! Originally created by Stephen Brawner (brawner@gmail.com)
Commit Version: 1.6.0-4-g7f85cfe Build Version: 1.6.7995.38578
For more information, please see http://wiki.ros.org/sw_urdf_exporter -->
<robot xmlns:xacro="http://ros.org/wiki/xacro">
<xacro:macro name="slide_w140" params="mesh_path:='' length:=0.1 min_d:=0.1 max_d:=0.1 slider_d:=0.14 parent_link:='' station_name:='' device_name:='' x:=0 y:=0 z:=0 r:=0" >
<joint name="${station_name}${device_name}base_link_joint" type="fixed">
<origin xyz="${x} ${y} ${z}" rpy="0 0 ${r}" />
<parent link="${parent_link}"/>
<child link="${station_name}${device_name}device_link"/>
<axis xyz="0 0 0"/>
</joint>
<link name="${station_name}${device_name}device_link"/>
<joint name="${station_name}${device_name}device_link_joint" type="fixed">
<origin xyz="${-min_d} 0 0" rpy="0 0 0" />
<parent link="${station_name}${device_name}device_link"/>
<child link="${station_name}${device_name}base_link"/>
<axis xyz="0 0 0"/>
</joint>
<link name="${station_name}${device_name}base_link">
<inertial>
<origin rpy="0 0 0" xyz="-0.00073944 -0.070732 0.035966"/>
<mass value="0.88697"/>
<inertia ixx="0.0019162" ixy="-2.5282E-11" ixz="6.1083E-07" iyy="0.00066605" iyz="1.4627E-08" izz="0.0019573"/>
</inertial>
<visual>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/base_link.STL"/>
</geometry>
<material name="">
<color rgba="1 1 1 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/base_link.STL"/>
</geometry>
</collision>
</link>
<link name="${station_name}${device_name}slider">
<inertial>
<origin rpy="0 0 0" xyz="0 -4.7184E-16 0.0095496"/>
<mass value="0.27913"/>
<inertia ixx="0.00048249" ixy="2.0866E-19" ixz="2.788E-21" iyy="0.00047014" iyz="1.1542E-17" izz="0.0009242"/>
</inertial>
<visual>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/slider.STL" />
</geometry>
<material name="">
<color rgba="1 1 1 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/slider.STL" />
</geometry>
</collision>
</link>
<joint name="${station_name}${device_name}slider_joint" type="prismatic">
<origin rpy="0 0 0" xyz="${min_d + slider_d/2} 0 0.0475"/>
<parent link="${station_name}${device_name}base_link"/>
<child link="${station_name}${device_name}slider"/>
<axis xyz="1 0 0"/>
<limit effort="100" lower="0" upper="${length}" velocity="1"/>
</joint>
<link name="${station_name}${device_name}length">
<inertial>
<origin rpy="0 0 0" xyz="0.005 -9.3044E-10 0.02803"/>
<mass value="0.050532"/>
<inertia ixx="8.098E-05" ixy="-1.2574E-19" ixz="3.1207E-20" iyy="4.4152E-06" iyz="-3.1294E-13" izz="7.7407E-05"/>
</inertial>
<visual>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/length.STL" scale="${(length + min_d + max_d + slider_d)} 1 1"/>
</geometry>
<material name="">
<color rgba="1 1 1 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/length.STL" scale="${(length + min_d + max_d + slider_d)} 1 1"/>
</geometry>
</collision>
</link>
<joint name="${station_name}${device_name}length_joint" type="fixed">
<origin rpy="0 0 0" xyz="0 0 0"/>
<parent link="${station_name}${device_name}base_link"/>
<child link="${station_name}${device_name}length"/>
</joint>
<link name="${station_name}${device_name}slide_end">
<inertial>
<origin rpy="0 0 0" xyz="0.003 8.6044E-06 0.035593"/>
<mass value="0.055452"/>
<inertia ixx="9.9729E-05" ixy="-1.0228E-21" ixz="3.8344E-22" iyy="2.3158E-05" iyz="1.5613E-08" izz="7.6904E-05"/>
</inertial>
<visual>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/slide_end.STL"/>
</geometry>
<material name="">
<color rgba="1 1 1 1"/>
</material>
</visual>
<collision>
<origin rpy="0 0 0" xyz="0 0 0"/>
<geometry>
<mesh filename="file://${mesh_path}/devices/slide_w140/meshes/slide_end.STL"/>
</geometry>
</collision>
</link>
<joint name="${station_name}${device_name}slide_end_joint" type="fixed">
<origin rpy="0 0 0" xyz="${length + max_d +min_d + slider_d} 0 0"/>
<parent link="${station_name}${device_name}base_link"/>
<child link="${station_name}${device_name}slide_end"/>
</joint>
</xacro:macro>
</robot>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,12 @@
{
"private_param":
{
"min_d": 0.1 ,
"max_d": 0.1 ,
"slider_d": 0.14
},
"public_param":
{
"length" :0.1
}
}

View File

@@ -0,0 +1,172 @@
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 = '''
<?xml version="1.0" encoding="UTF-8"?>
<robot name="minimal">
</robot>
'''
self.robot_state_str= '''<?xml version="1.0" ?>
<robot xmlns:xacro="http://ros.org/wiki/xacro" name="full_dev">
<link name="world"/>
</robot>
'''
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 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("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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

View File

@@ -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"
]
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" ?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="generic_labware_tube_10_75">
<link name='0_base_link'>
<visual name='visual'>
<geometry>
<mesh filename="meshes/0_base.stl"/>
</geometry>
<material name="clay" />
</visual>
</link>
</robot>

View File

@@ -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"
]
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" ?>
<robot xmlns:xacro="http://www.ros.org/wiki/xacro" name="tecan_nested_tip_rack">
<link name='plate_link'>
<visual name='visual'>
<geometry>
<mesh filename="meshes/plate.stl"/>
</geometry>
<material name="clay" />
</visual>
</link>
</robot>

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

View File

@@ -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: <Fixed 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
deck_device_link:
Alpha: 1
Show Axes: false
Show Trail: false
deck_first_link:
Alpha: 1
Show Axes: false
Show Trail: false
Value: true
deck_fourth_link:
Alpha: 1
Show Axes: false
Show Trail: false
Value: true
deck_main_link:
Alpha: 1
Show Axes: false
Show Trail: false
Value: true
deck_second_link:
Alpha: 1
Show Axes: false
Show Trail: false
Value: true
deck_socketTypeGenericSbsFootprint:
Alpha: 1
Show Axes: false
Show Trail: false
deck_socketTypeHEPAModule:
Alpha: 1
Show Axes: false
Show Trail: false
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
deck_device_link:
Alpha: 1
Show Axes: false
Show Trail: false
deck_first_link:
Alpha: 1
Show Axes: false
Show Trail: false
Value: true
deck_fourth_link:
Alpha: 1
Show Axes: false
Show Trail: false
Value: true
deck_main_link:
Alpha: 1
Show Axes: false
Show Trail: false
Value: true
deck_second_link:
Alpha: 1
Show Axes: false
Show Trail: false
Value: true
deck_socketTypeGenericSbsFootprint:
Alpha: 1
Show Axes: false
Show Trail: false
deck_socketTypeHEPAModule:
Alpha: 1
Show Axes: false
Show Trail: false
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.49553644657135
Enable Stereo Rendering:
Stereo Eye Separation: 0.05999999865889549
Stereo Focal Distance: 1
Swap Stereo Eyes: false
Value: false
Focal Point:
X: 1.0440783500671387
Y: 0.27775266766548157
Z: 0.42559614777565
Focal Shape Fixed Size: true
Focal Shape Size: 0.05000000074505806
Invert Z Axis: false
Name: Current View
Near Clip Distance: 0.009999999776482582
Pitch: 0.40479573607444763
Target Frame: <Fixed Frame>
Value: Orbit (rviz)
Yaw: 6.090748310089111
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

View File

@@ -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):

View File

View File

@@ -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 ilabos_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()

View File

@@ -19,7 +19,9 @@ gripper.mock:
result:
position: position
effort: torque
model:
type: device
mesh: opentrons_liquid_handler
gripper.misumi_rz:
description: Misumi RZ gripper
@@ -35,4 +37,4 @@ gripper.misumi_rz:
command: command
feedback: {}
result:
success: success
success: success

View File

@@ -0,0 +1,5 @@
lh_joint_publisher:
class:
module: unilabos.devices.ros_dev.liquid_handler_joint_publisher:LiquidHandlerJointPublisher
type: ros2

View File

@@ -2,4 +2,7 @@ OTDeck:
description: Opentrons deck
class:
module: pylabrobot.resources.opentrons.deck:OTDeck
type: pylabrobot
type: pylabrobot
model:
type: device
mesh: opentrons_liquid_handler

View File

@@ -51,6 +51,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

View File

@@ -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:

View File

@@ -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
@@ -44,13 +48,15 @@ def main(
graph: Optional[Dict[str, Any]] = None,
controllers_config: Dict[str, Any] = {},
bridges: List[Any] = [],
visual: str = "None",
resources_mesh_config: dict = {},
args: List[str] = ["--log-level", "debug"],
discovery_interval: float = 5.0,
) -> None:
"""主函数"""
rclpy.init(args=args)
rclpy.__executor = executor = MultiThreadedExecutor()
rclpy.init(args=args)
executor = rclpy.__executor = MultiThreadedExecutor()
# 创建主机节点
host_node = HostNode(
"host_node",
@@ -62,11 +68,26 @@ def main(
discovery_interval,
)
if visual != "None":
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(
@@ -78,8 +99,11 @@ def slave(
args: List[str] = ["--log-level", "debug"],
) -> None:
"""从节点函数"""
rclpy.init(args=args)
rclpy.__executor = executor = MultiThreadedExecutor()
if not rclpy.ok():
rclpy.init(args=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)
@@ -120,7 +144,7 @@ def slave(
logger.info(f"Slave resource added.")
while True:
input()
time.sleep(1)
if __name__ == "__main__":
main()

View File

@@ -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()

View File

@@ -0,0 +1,956 @@
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
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
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 = None
self.__old_planning_scene = None
self.__old_allowed_collision_matrix = None
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
)
self.resource_mesh_setup()
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()
# time.sleep(1)
def resource_mesh_setup(self):
"""move_group初始化完成后的设置"""
self.get_logger().info('开始设置资源网格管理器')
#遍历resource_config中的资源配置判断panent是否在resource_model中
for resource_id, resource_config in self.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 not in self.resource_model and parent is not None and resource_config['class'] != '':
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'])
)
# print("-"*20)
# print(f"resource_id: {resource_id}")
# print(f"parent: {parent}")
# print(f"resource_config: {resource_config}")
# print(f"parent_link: {parent_link}")
# print("-"*20)
rotation = {
"x": q[0],
"y": q[1],
"z": q[2],
"w": q[3]
}
# 更新资源TF字典
self.resource_tf_dict[resource_id] = {
"parent": parent_link,
"position": position,
"rotation": rotation
}
def publish_resource_tf(self):
"""
发布资源之间的TF关系
遍历self.resource_tf_dict中的每个元素根据keyparent以及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("'",'"'))
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)
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_model中有对应模型的资源添加碰撞网格
该方法检查每个资源ID是否在self.resource_model中有对应的3D模型文件路径
如果有则调用add_collision_mesh方法将其添加到碰撞环境中。
"""
self.get_logger().info('开始添加资源碰撞网格')
for resource_id, tf_info in self.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])
)
# 添加碰撞网格
self.add_collision_mesh(
filepath=self.resource_model[resource_id]['mesh'],
id=resource_id,
position=position,
quat_xyzw=q,
frame_id=resource_id
)
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])
)
# 添加碰撞网格
self.add_collision_mesh(
filepath=self.resource_model[id_]['mesh'],
id=resource_id,
position=position,
quat_xyzw=q,
frame_id=resource_id
)
time.sleep(0.03)
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 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)
)

View File

@@ -48,6 +48,7 @@ class DeviceNodeResourceTracker:
def loop_find_resource(self, resource, resource_cls_type, identifier_key, compare_value):
res_list = []
print(resource, 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))