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..9b1debed --- /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": 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": [ + + ] +} \ No newline at end of file diff --git a/unilabos/app/backend.py b/unilabos/app/backend.py index 19cebff0..8acc8251 100644 --- a/unilabos/app/backend.py +++ b/unilabos/app/backend.py @@ -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.") diff --git a/unilabos/app/main.py b/unilabos/app/main.py index 89462822..16667d6c 100644 --- a/unilabos/app/main.py +++ b/unilabos/app/main.py @@ -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__": diff --git a/unilabos/app/mq.py b/unilabos/app/mq.py index a6123fb2..d0975e91 100644 --- a/unilabos/app/mq.py +++ b/unilabos/app/mq.py @@ -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)) 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..2ec28b01 --- /dev/null +++ b/unilabos/device_mesh/resource_visalization.py @@ -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 = ''' + + + + + ''' + 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 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() \ 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..bf32efc3 --- /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 + 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: + 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 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/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..7933f8df --- /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 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() \ No newline at end of file diff --git a/unilabos/registry/devices/robot_gripper.yaml b/unilabos/registry/devices/robot_gripper.yaml index 04ea338b..3cb86fed 100644 --- a/unilabos/registry/devices/robot_gripper.yaml +++ b/unilabos/registry/devices/robot_gripper.yaml @@ -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 \ 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/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..c00b2744 100644 --- a/unilabos/registry/resources/opentrons/plates.yaml +++ b/unilabos/registry/resources/opentrons/plates.yaml @@ -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 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/ros/main_slave_run.py b/unilabos/ros/main_slave_run.py index 9ac96748..8560d376 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 @@ -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() 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..012404b3 --- /dev/null +++ b/unilabos/ros/nodes/presets/resource_mesh_manager.py @@ -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中的每个元素,根据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("'",'"')) + 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) + ) diff --git a/unilabos/ros/nodes/resource_tracker.py b/unilabos/ros/nodes/resource_tracker.py index c0886cd4..dc9f9c4a 100644 --- a/unilabos/ros/nodes/resource_tracker.py +++ b/unilabos/ros/nodes/resource_tracker.py @@ -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))