diff --git a/ros2/test/integration/ros2__test_qos.cpp b/ros2/test/integration/ros2__test_qos.cpp index 1ee8cbc..5f83a07 100644 --- a/ros2/test/integration/ros2__test_qos.cpp +++ b/ros2/test/integration/ros2__test_qos.cpp @@ -124,7 +124,7 @@ class ROS2QoS : public ::testing::Test ASSERT_EQ(msg_future.wait_for(0s), std::future_status::ready); xtypes::DynamicData received_msg = msg_future.get(); - EXPECT_EQ(received_msg.type().name(), "std_msgs/String"); + EXPECT_EQ(received_msg.type().name(), "std_msgs/msg/String"); xtypes::ReadableDynamicDataRef xtypes_msg = received_msg; typename std_msgs::msg::String::_data_type ros2_field; @@ -165,7 +165,7 @@ TEST_F(ROS2QoS, Durability_Incompatible_QoS) yaml += " mock_to_ros2: { from: mock, to: ros2 }\n"; yaml += " ros2_to_mock: { from: ros2, to: mock }\n"; yaml += "topics:\n"; - yaml += " transmit: { type: 'std_msgs/String', route: ros2_to_mock, "; + yaml += " transmit: { type: 'std_msgs/msg/String', route: ros2_to_mock, "; yaml += " ros2: { qos: { durability: TRANSIENT_LOCAL } } }\n"; set_up(yaml); @@ -188,7 +188,7 @@ TEST_F(ROS2QoS, Durability_Compatible_QoS) yaml += " mock_to_ros2: { from: mock, to: ros2 }\n"; yaml += " ros2_to_mock: { from: ros2, to: mock }\n"; yaml += "topics:\n"; - yaml += " transmit: { type: 'std_msgs/String', route: ros2_to_mock, "; + yaml += " transmit: { type: 'std_msgs/msg/String', route: ros2_to_mock, "; yaml += " ros2: { qos: { durability: VOLATILE } } }\n"; set_up(yaml); @@ -211,7 +211,7 @@ TEST_F(ROS2QoS, Deadline_Incompatible_QoS) yaml += " mock_to_ros2: { from: mock, to: ros2 }\n"; yaml += " ros2_to_mock: { from: ros2, to: mock }\n"; yaml += "topics:\n"; - yaml += " transmit: { type: 'std_msgs/String', route: ros2_to_mock, "; + yaml += " transmit: { type: 'std_msgs/msg/String', route: ros2_to_mock, "; yaml += " ros2: { qos: { deadline: { sec: 1 } } } }\n"; set_up(yaml); @@ -234,7 +234,7 @@ TEST_F(ROS2QoS, Deadline_Compatible_QoS) yaml += " mock_to_ros2: { from: mock, to: ros2 }\n"; yaml += " ros2_to_mock: { from: ros2, to: mock }\n"; yaml += "topics:\n"; - yaml += " transmit: { type: 'std_msgs/String', route: ros2_to_mock, "; + yaml += " transmit: { type: 'std_msgs/msg/String', route: ros2_to_mock, "; yaml += " ros2: { qos: { deadline: { sec: 2 } } } }\n"; set_up(yaml); @@ -257,7 +257,7 @@ TEST_F(ROS2QoS, Liveliness_Incompatible_QoS) yaml += " mock_to_ros2: { from: mock, to: ros2 }\n"; yaml += " ros2_to_mock: { from: ros2, to: mock }\n"; yaml += "topics:\n"; - yaml += " transmit: { type: 'std_msgs/String', route: ros2_to_mock, "; + yaml += " transmit: { type: 'std_msgs/msg/String', route: ros2_to_mock, "; yaml += " ros2: { qos: { liveliness: { kind: MANUAL_BY_TOPIC } } } }\n"; set_up(yaml); @@ -280,7 +280,7 @@ TEST_F(ROS2QoS, Liveliness_Compatible_QoS) yaml += " mock_to_ros2: { from: mock, to: ros2 }\n"; yaml += " ros2_to_mock: { from: ros2, to: mock }\n"; yaml += "topics:\n"; - yaml += " transmit: { type: 'std_msgs/String', route: ros2_to_mock, "; + yaml += " transmit: { type: 'std_msgs/msg/String', route: ros2_to_mock, "; yaml += " ros2: { qos: { liveliness: { kind: AUTOMATIC } } } }\n"; set_up(yaml); @@ -303,7 +303,7 @@ TEST_F(ROS2QoS, Lease_Duration_Incompatible_QoS) yaml += " mock_to_ros2: { from: mock, to: ros2 }\n"; yaml += " ros2_to_mock: { from: ros2, to: mock }\n"; yaml += "topics:\n"; - yaml += " transmit: { type: 'std_msgs/String', route: ros2_to_mock, "; + yaml += " transmit: { type: 'std_msgs/msg/String', route: ros2_to_mock, "; yaml += " ros2: { qos: { liveliness: { sec: 1 } } } }\n"; set_up(yaml); @@ -325,7 +325,7 @@ TEST_F(ROS2QoS, Lease_Duration_Compatible_QoS) yaml += " mock_to_ros2: { from: mock, to: ros2 }\n"; yaml += " ros2_to_mock: { from: ros2, to: mock }\n"; yaml += "topics:\n"; - yaml += " transmit: { type: 'std_msgs/String', route: ros2_to_mock, "; + yaml += " transmit: { type: 'std_msgs/msg/String', route: ros2_to_mock, "; yaml += " ros2: { qos: { liveliness: { sec: 3 } } } }\n"; set_up(yaml); @@ -348,7 +348,7 @@ TEST_F(ROS2QoS, Reliability_Incompatible_QoS) yaml += " mock_to_ros2: { from: mock, to: ros2 }\n"; yaml += " ros2_to_mock: { from: ros2, to: mock }\n"; yaml += "topics:\n"; - yaml += " transmit: { type: 'std_msgs/String', route: ros2_to_mock, "; + yaml += " transmit: { type: 'std_msgs/msg/String', route: ros2_to_mock, "; yaml += " ros2: { qos: { reliability: RELIABLE } } }\n"; set_up(yaml); @@ -371,7 +371,7 @@ TEST_F(ROS2QoS, Reliability_Compatible_QoS) yaml += " mock_to_ros2: { from: mock, to: ros2 }\n"; yaml += " ros2_to_mock: { from: ros2, to: mock }\n"; yaml += "topics:\n"; - yaml += " transmit: { type: 'std_msgs/String', route: ros2_to_mock, "; + yaml += " transmit: { type: 'std_msgs/msg/String', route: ros2_to_mock, "; yaml += " ros2: { qos: { reliability: BEST_EFFORT } } }\n"; set_up(yaml); diff --git a/ros2/test/resources/ros2__domain_change.yaml b/ros2/test/resources/ros2__domain_change.yaml index 8b5150f..2c33cef 100644 --- a/ros2/test/resources/ros2__domain_change.yaml +++ b/ros2/test/resources/ros2__domain_change.yaml @@ -6,4 +6,4 @@ routes: domain_5_to_10: { from: ros2_domain5, to: ros2_domain10 } topics: - string_topic: { type: "std_msgs/String", route: domain_5_to_10 } + string_topic: { type: "std_msgs/msg/String", route: domain_5_to_10 } diff --git a/ros2/test/resources/ros2__geometry_msgs__pubsub.yaml b/ros2/test/resources/ros2__geometry_msgs__pubsub.yaml index 343f45b..35e865f 100644 --- a/ros2/test/resources/ros2__geometry_msgs__pubsub.yaml +++ b/ros2/test/resources/ros2__geometry_msgs__pubsub.yaml @@ -7,5 +7,5 @@ routes: ros2_to_mock: { from: ros2, to: mock } topics: - transmit_pose: { type: "geometry_msgs/Pose", route: ros2_to_mock } - echo_pose: { type: "geometry_msgs/Pose", route: mock_to_ros2 } + transmit_pose: { type: "geometry_msgs/msg/Pose", route: ros2_to_mock } + echo_pose: { type: "geometry_msgs/msg/Pose", route: mock_to_ros2 } diff --git a/ros2/test/resources/ros2__geometry_msgs__services.yaml b/ros2/test/resources/ros2__geometry_msgs__services.yaml index b6f3e19..8e0521f 100644 --- a/ros2/test/resources/ros2__geometry_msgs__services.yaml +++ b/ros2/test/resources/ros2__geometry_msgs__services.yaml @@ -7,5 +7,5 @@ routes: ros2_srv: { server: ros2, clients: mock } services: - get_plan: { type: "nav_msgs/GetPlan:request", route: ros2_srv } - echo_plan: { type: "nav_msgs/GetPlan:response", route: mock_srv } + get_plan: { type: "nav_msgs/srv/GetPlan:request", route: ros2_srv } + echo_plan: { type: "nav_msgs/srv/GetPlan:response", route: mock_srv } diff --git a/ros2/test/resources/ros2__websocket__test.yaml b/ros2/test/resources/ros2__websocket__test.yaml index 6eba835..6e0f543 100644 --- a/ros2/test/resources/ros2__websocket__test.yaml +++ b/ros2/test/resources/ros2__websocket__test.yaml @@ -14,4 +14,4 @@ routes: ros2_to_websocket: { from: ros2, to: websocket } topics: - helloworld: { type: "std_msgs/String", route: websocket_to_ros2, remap: {websocket: {type: std_msgs__String}} } + helloworld: { type: "std_msgs/msg/String", route: websocket_to_ros2, remap: {websocket: {type: std_msgs__String}} } diff --git a/utils/ros2-mix-generator/cmake/is_ros2_rosidl_mix.cmake b/utils/ros2-mix-generator/cmake/is_ros2_rosidl_mix.cmake index 1f52281..042b9c8 100644 --- a/utils/ros2-mix-generator/cmake/is_ros2_rosidl_mix.cmake +++ b/utils/ros2-mix-generator/cmake/is_ros2_rosidl_mix.cmake @@ -66,12 +66,16 @@ function(is_ros2_rosidl_mix) endif() endforeach() + if(NOT Python_EXECUTABLE) + find_package(Python COMPONENTS Interpreter) + endif() + is_mix_generator( IDL_TYPE rosidl SCRIPT INTERPRETER - ${PYTHON_EXECUTABLE} + ${Python_EXECUTABLE} FIND ${CMAKE_CURRENT_LIST_DIR}/scripts/is_ros2_rosidl_find_package_info.py GENERATE diff --git a/utils/ros2-mix-generator/resources/convert__msg.cpp.em b/utils/ros2-mix-generator/resources/convert__msg.cpp.em index a4dbf43..ed02e7b 100644 --- a/utils/ros2-mix-generator/resources/convert__msg.cpp.em +++ b/utils/ros2-mix-generator/resources/convert__msg.cpp.em @@ -5,8 +5,8 @@ @# EmPy template for generating is/rosidl/ros2//src/msg/convert__msg__.cpp files @# @# Context: -@# - spec (rosidl_parser.MessageSpecification) -@# Parsed specification of the .msg file +@# - spec (rosidl_adapter.parser.MessageSpecification) +@# Parsed specification of the .msg/.idl file @# - subfolder (string) @# The subfolder / subnamespace of the message @# Either 'msg' or 'srv' diff --git a/utils/ros2-mix-generator/resources/convert__msg.hpp.em b/utils/ros2-mix-generator/resources/convert__msg.hpp.em index ee5a340..7ea0d06 100644 --- a/utils/ros2-mix-generator/resources/convert__msg.hpp.em +++ b/utils/ros2-mix-generator/resources/convert__msg.hpp.em @@ -5,8 +5,8 @@ @# EmPy template for generating is/rosidl/ros2//include/is/rosidl/ros2//msg/convert__msg__.hpp files @# @# Context: -@# - spec (rosidl_parser.MessageSpecification) -@# Parsed specification of the .msg file +@# - spec (rosidl_adapter.parser.MessageSpecification) +@# Parsed specification of the .msg/.idl file @# - subfolder (string) @# The subfolder / subnamespace of the message @# Either 'msg' or 'srv' @@ -20,7 +20,7 @@ underscore_msg_type = get_header_filename_from_msg_name(camelcase_msg_type) cpp_msg_type = '{}::msg::{}'.format( spec.base_type.pkg_name, camelcase_msg_type) -msg_type_string = '{}/{}'.format( +msg_type_string = '{}/msg/{}'.format( spec.base_type.pkg_name, camelcase_msg_type) header_guard_parts = [ @@ -104,27 +104,19 @@ inline const eprosima::xtypes::StructType& type() } //============================================================================== -inline void convert_to_ros2(const eprosima::xtypes::ReadableDynamicDataRef& from, Ros2_Msg& to) +inline void convert_to_ros2([[maybe_unused]] const eprosima::xtypes::ReadableDynamicDataRef& from, [[maybe_unused]] Ros2_Msg& to) { @[for field in alphabetical_fields]@ utils::Convert::from_xtype_field(from["@(field.name)"], to.@(field.name)); @[end for]@ - - // Suppress possible unused variable warnings - (void)from; - (void)to; } //============================================================================== -inline void convert_to_xtype(const Ros2_Msg& from, eprosima::xtypes::WritableDynamicDataRef to) +inline void convert_to_xtype([[maybe_unused]] const Ros2_Msg& from, [[maybe_unused]]eprosima::xtypes::WritableDynamicDataRef to) { @[for field in alphabetical_fields]@ utils::Convert::to_xtype_field(from.@(field.name), to["@(field.name)"]); @[end for]@ - - // Suppress possible unused variable warnings - (void)from; - (void)to; } static eprosima::is::utils::Logger logger ("is::sh::ROS2"); diff --git a/utils/ros2-mix-generator/resources/convert__srv.cpp.em b/utils/ros2-mix-generator/resources/convert__srv.cpp.em index 9b80c6e..11ae7b2 100644 --- a/utils/ros2-mix-generator/resources/convert__srv.cpp.em +++ b/utils/ros2-mix-generator/resources/convert__srv.cpp.em @@ -5,8 +5,8 @@ @# EmPy template for generating is/rosidl/ros2//src/srv/convert__srv__.cpp files @# @# Context: -@# - spec (rosidl_parser.ServiceSpecification) -@# Parsed specification of the .srv file +@# - spec (rosidl_adapter.parser.ServiceSpecification) +@# Parsed specification of the .srv/.idl file @# - get_header_filename_from_msg_name (function) @####################################################################### @@ -16,7 +16,7 @@ underscore_srv_type = get_header_filename_from_msg_name(camelcase_srv_type) cpp_srv_type = '{}::srv::{}'.format(spec.pkg_name, camelcase_srv_type) -srv_type_string = '{}/{}'.format(spec.pkg_name, camelcase_srv_type) +srv_type_string = '{}/srv/{}'.format(spec.pkg_name, camelcase_srv_type) namespace_parts_srv = [ 'convert', spec.pkg_name, 'srv', underscore_srv_type] diff --git a/utils/ros2-mix-generator/scripts/is_ros2_rosidl_find_package_info.py b/utils/ros2-mix-generator/scripts/is_ros2_rosidl_find_package_info.py index c9b64c9..203e9d2 100644 --- a/utils/ros2-mix-generator/scripts/is_ros2_rosidl_find_package_info.py +++ b/utils/ros2-mix-generator/scripts/is_ros2_rosidl_find_package_info.py @@ -4,10 +4,13 @@ import argparse import os import sys +from pathlib import Path try: from rosidl_adapter.parser import parse_message_file from rosidl_adapter.parser import parse_service_file + from rosidl_parser.parser import parse_idl_file + from rosidl_parser.definition import IdlLocator, NamespacedType, Message, Service except ImportError: print('Unable to import rosidl_adapter. Please source a ROS2 installation first.', end='', file=sys.stderr) @@ -30,31 +33,35 @@ def find_package_info(requested_pkg_name): share_dir = get_package_share_directory(requested_pkg_name) - message_dir = '{}/msg'.format(share_dir) - if os.path.exists(message_dir): - for relative_msg_file in os.listdir(message_dir): - if not relative_msg_file.endswith('.msg'): - continue - - msg_file = '{}/{}'.format(message_dir, relative_msg_file) - + message_dir = Path(share_dir) / 'msg' + if message_dir.exists(): + for msg_file in message_dir.glob("*.msg"): info.msg_files.append(msg_file) + msg = parse_message_file(requested_pkg_name, msg_file) for field in msg.fields: - if field.type.is_primitive_type(): - continue + if not field.type.is_primitive_type(): + info.dependencies.append(field.type.pkg_name) - info.dependencies.append(field.type.pkg_name) + ignore_msgs = {p.stem for p in info.msg_files} - service_dir = '{}/srv'.format(share_dir) - if os.path.exists(service_dir): - for relative_srv_file in os.listdir(service_dir): - if not relative_srv_file.endswith('.srv'): + for msg_file in message_dir.glob("*.idl"): + if msg_file.stem in ignore_msgs: continue - srv_file = '{}/{}'.format(service_dir, relative_srv_file) + info.msg_files.append(msg_file) + + idl_file = parse_idl_file(IdlLocator(share_dir, msg_file.relative_to(share_dir))) + idl_msg = idl_file.content.get_elements_of_type(Message)[0] + for member in idl_msg.structure.members: + if isinstance(member.type, NamespacedType): + info.dependencies.append(member.type.namespaces[0]) + service_dir = Path(share_dir) / 'srv' + if service_dir.exists(): + for srv_file in service_dir.glob("*.srv"): info.srv_files.append(srv_file) + srv = parse_service_file(requested_pkg_name, srv_file) for component in [srv.request, srv.response]: for field in component.fields: @@ -63,6 +70,21 @@ def find_package_info(requested_pkg_name): info.dependencies.append(field.type.pkg_name) + ignore_srvs = {p.stem for p in info.srv_files} + + for srv_file in service_dir.glob("*.idl"): + if srv_file.stem in ignore_srvs: + continue + + info.srv_files.append(srv_file) + + idl_file = parse_idl_file(IdlLocator(share_dir, srv_file.relative_to(share_dir))) + idl_srv = idl_file.content.get_elements_of_type(Service)[0] + for members in [idl_srv.request_message.structure.members, idl_srv.response_message.structure.members]: + for member in members: + if isinstance(member.type, NamespacedType): + info.dependencies.append(member.type.namespaces[0]) + return info @@ -89,10 +111,10 @@ def print_package_info(root_pkg_name, pkg_info_dict): dependency_list_str = '#'.join(dependency_list) message_files = pkg_info_dict[root_pkg_name].msg_files - message_files_str = '#'.join(message_files) + message_files_str = '#'.join(str(f) for f in message_files) service_files = pkg_info_dict[root_pkg_name].srv_files - service_files_str = '#'.join(service_files) + service_files_str = '#'.join(str(f) for f in service_files) file_dependencies = [] for pkg, info in pkg_info_dict.items(): @@ -100,7 +122,7 @@ def print_package_info(root_pkg_name, pkg_info_dict): if pkg == root_pkg_name: file_dependencies.extend(info.srv_files) - file_dependencies_str = '#'.join(file_dependencies) + file_dependencies_str = '#'.join(str(f) for f in file_dependencies) output_str = ';'.join([dependency_list_str, message_files_str, service_files_str, file_dependencies_str]) print(output_str) diff --git a/utils/ros2-mix-generator/scripts/is_ros2_rosidl_generate.py b/utils/ros2-mix-generator/scripts/is_ros2_rosidl_generate.py index 8cfc576..378c6fb 100644 --- a/utils/ros2-mix-generator/scripts/is_ros2_rosidl_generate.py +++ b/utils/ros2-mix-generator/scripts/is_ros2_rosidl_generate.py @@ -8,16 +8,19 @@ import os import sys import subprocess +from pathlib import Path + try: - from rosidl_adapter.parser import parse_message_file - from rosidl_adapter.parser import parse_service_file + from rosidl_adapter.parser import parse_message_file, parse_service_file from rosidl_cmake import convert_camel_case_to_lower_case_underscore except ImportError: print('Unable to import rosidl_adapter. Please source a ROS2 installation first.', end='', file=sys.stderr) sys.exit(1) +from rosidl_parser_ext import parse_idl_to_message_spec, parse_idl_to_service_spec + def get_message_type_name(spec): try: @@ -27,7 +30,6 @@ def get_message_type_name(spec): def generate_file(template, destination, context): - base_name = template.split('/')[-1] base_name_components = base_name.split('.') @@ -50,10 +52,12 @@ def generate_file(template, destination, context): with open(output_file_path, 'w') as file: file.write(output_buffer.getvalue()) + def sys_call(command): dev_null = open(os.devnull, 'w') return subprocess.check_output((command).split(), universal_newlines = True, stderr = dev_null) + def find_idl_include_paths(): environment = sys_call("env") for line in environment.splitlines(): @@ -65,8 +69,9 @@ def find_idl_include_paths(): idl_includes += include + "/share " return idl_includes + def get_idl_from_file(idl_file, includes): - preprocess_cmd = "cpp -H " + includes + " " + idl_file + preprocess_cmd = "cpp -H " + includes + " " + str(idl_file) unrolled_idl = sys_call(preprocess_cmd) idl = "" for line in unrolled_idl.splitlines(): @@ -74,14 +79,29 @@ def get_idl_from_file(idl_file, includes): idl += line + "\n" return idl -def generate_files(package, source_dir, header_dir, idl_files, cpp_files, hpp_files, prefix, parse_fnc): + +def generate_files(package, source_dir, header_dir, idl_files, cpp_files, hpp_files, prefix): includes = find_idl_include_paths() for idl_file in idl_files: - idl = get_idl_from_file(idl_file[:-3] + "idl", includes) + idl_file = Path(idl_file) + + idl_contents = get_idl_from_file(idl_file.with_suffix('.idl'), includes) + + spec = None + if idl_file.suffix == '.idl': + if prefix == 'msg': + spec = parse_idl_to_message_spec(package, str(idl_file)) + elif prefix == 'srv': + spec = parse_idl_to_service_spec(package, str(idl_file)) + else: + if prefix == 'msg': + spec = parse_message_file(package, str(idl_file)) + elif prefix == 'srv': + spec = parse_service_file(package, str(idl_file)) context = { - 'idl': idl, - 'spec': parse_fnc(package, idl_file), + 'idl': idl_contents, + 'spec': spec, 'subdir': prefix, 'get_header_filename_from_msg_name': convert_camel_case_to_lower_case_underscore } @@ -116,11 +136,11 @@ def main(cli_args): generate_files(args.package, args.source_dir, args.header_dir, args.msg_idl_files, args.msg_cpp_files, args.msg_hpp_files, - 'msg', parse_message_file) + 'msg') generate_files(args.package, args.source_dir, args.header_dir, args.srv_idl_files, args.srv_cpp_files, args.srv_hpp_files, - 'srv', parse_service_file) + 'srv') if __name__ == '__main__': diff --git a/utils/ros2-mix-generator/scripts/rosidl_parser_ext.py b/utils/ros2-mix-generator/scripts/rosidl_parser_ext.py new file mode 100644 index 0000000..ff46c66 --- /dev/null +++ b/utils/ros2-mix-generator/scripts/rosidl_parser_ext.py @@ -0,0 +1,127 @@ +from pathlib import Path +from types import NoneType +from typing import Any, List, Type + +from ament_index_python.packages import get_package_share_directory + +from rosidl_parser.parser import parse_idl_file +import rosidl_parser.definition as idl_def +import rosidl_adapter.parser as rosidl_parser +from rosidl_adapter.msg import MSG_TYPE_TO_IDL + +IDL_TYPE_TO_MSG = {v: k for k, v in MSG_TYPE_TO_IDL.items()} + +BOUNDED_TYPES = [idl_def.BoundedSequence, idl_def.Array] +BRACKET_TYPES = BOUNDED_TYPES + [idl_def.UnboundedSequence, idl_def.Array] +STRING_BOUND_TYPES = [idl_def.BoundedString, idl_def.BoundedWString] + + +def resolve_typename(member_type: Type[idl_def.AbstractType]) -> str: + if isinstance(member_type, idl_def.BasicType): + return member_type.typename + elif isinstance(member_type, idl_def.AbstractWString): + return "wstring" + elif isinstance(member_type, idl_def.AbstractString): + return "string" + elif isinstance(member_type, idl_def.NamedType): + return member_type.name + elif isinstance(member_type, idl_def.AbstractNestedType): + return resolve_typename(member_type.value_type) + else: + return member_type.name + + +def build_type_string(member_type: Type[idl_def.AbstractType]) -> str: + type_string = resolve_typename(member_type) + + if isinstance(member_type, idl_def.AbstractNestedType): + if isinstance(member_type.value_type, idl_def.NamespacedType): + type_string = f"{member_type.value_type.namespaces[0]}/{type_string}" + + if type_string in IDL_TYPE_TO_MSG: + type_string = IDL_TYPE_TO_MSG[type_string] + + has_string_bounds = any(isinstance(member_type, t) for t in STRING_BOUND_TYPES) + has_brackets = any(isinstance(member_type, t) for t in BRACKET_TYPES) + + if has_string_bounds: + type_string += f"{rosidl_parser.STRING_UPPER_BOUND_TOKEN}{member_type.string_upper_bound}" + + if has_brackets: + bounds = '' + if isinstance(member_type, idl_def.BoundedSequence): + bounds = f"{rosidl_parser.ARRAY_UPPER_BOUND_TOKEN}{member_type.maximum_size}" + if isinstance(member_type, idl_def.Array): + bounds = f"{member_type.size}" + type_string += f"[{bounds}]" + + return type_string + + +def find_annotation_value(annotations: List, name: str, value_key="value") -> Any: + for a in annotations: + if a.name == name: + return a.value[value_key] + + return None + + +def process_constants(constants: List[idl_def.Constant]) -> List[rosidl_parser.Constant]: + out_constants = [] + for c in constants: + typename = IDL_TYPE_TO_MSG[c.type.typename] + out_constants.append(rosidl_parser.Constant(typename, c.name, c.value)) + return out_constants + + +def process_members(members: List) -> List[rosidl_parser.Field]: + fields = [] + for m in members: + type_pkg_name = None + type_string = build_type_string(m.type) + + if isinstance(m.type, idl_def.NamedType): + type_pkg_name = m.type.namespaces[0] + + field_type = rosidl_parser.Type(type_string, type_pkg_name) + + default_value_str = find_annotation_value(m.annotations, 'default') + if type(default_value_str) not in [NoneType, str]: + default_value_str = str(default_value_str) + + fields.append(rosidl_parser.Field(field_type, m.name, default_value_str)) + + return fields + + +def parse_idl_message(pkg_name: str, msg_name: str, idl_msg: idl_def.Message) -> rosidl_parser.MessageSpecification: + fields = process_members(idl_msg.structure.members) + constants = process_constants(idl_msg.constants) + + msg = rosidl_parser.MessageSpecification(pkg_name, msg_name, fields, constants) + # msg.annotations['comment'] = message_comments + + return msg + + +def parse_idl_to_message_spec(pkg_name: str, interface_file_path: str) -> rosidl_parser.MessageSpecification: + path = Path(interface_file_path) + share_dir = get_package_share_directory(pkg_name) + msg_name = path.stem + + idl_file = parse_idl_file(idl_def.IdlLocator(share_dir, path.relative_to(share_dir))) + idl_msg = idl_file.content.get_elements_of_type(idl_def.Message)[0] + return parse_idl_message(pkg_name, msg_name, idl_msg) + + +def parse_idl_to_service_spec(pkg_name: str, interface_file_path: str) -> rosidl_parser.ServiceSpecification: + path = Path(interface_file_path) + share_dir = get_package_share_directory(pkg_name) + srv_name = path.stem + + idl_file = parse_idl_file(idl_def.IdlLocator(share_dir, path.relative_to(share_dir))) + idl_srv = idl_file.content.get_elements_of_type(idl_def.Service)[0] + request_message = parse_idl_message(pkg_name, srv_name + rosidl_parser.SERVICE_REQUEST_MESSAGE_SUFFIX, idl_srv.request_message) + response_message = parse_idl_message(pkg_name, srv_name + rosidl_parser.SERVICE_RESPONSE_MESSAGE_SUFFIX, idl_srv.response_message) + + return rosidl_parser.ServiceSpecification(pkg_name, srv_name, request_message, response_message)