Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
// Copyright 2019 Open Source Robotics Foundation, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
* Modifications copyright (C) 2021 Leidos
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, like this applied to the CPP file above.

* - Converted into Lifecycle Component Wrapper
*
*/

/** \mainpage rclcpp_components: Package containing tools for dynamically loadable components.
*
* - LifecycleComponentWrapper: Node to manage components. It has the services to load, unload and list
* current components.
* - rclcpp_components/lifecycle_component_wrapper.hpp)
* - Node factory: The NodeFactory interface is used by the class loader to instantiate components.
* - rclcpp_components/node_factory.hpp)
* - It allows for classes not derived from `rclcpp::Node` to be used as components.
* - It allows derived constructors to be called when components are loaded.
* - NOTE: CARMA Change The nodes will be loaded on activation of this lifecycle node and unloaded on deactivation.
* This means that this component wrapper can be used to give non-lifecycle nodes a minimal form of lifecycle management.
*
* Some useful abstractions and utilities:
* - [RCLCPP_COMPONENTS_REGISTER_NODE: Register a component that can be dynamically loaded
* at runtime.
* - (include/rclcpp_components/register_node_macro.hpp)
*
* Some useful internal abstractions and utilities:
* - Macros for controlling symbol visibility on the library
* - rclcpp_components/visibility_control.h
*
* Package containing CMake tools for register components:
* - `rclcpp_components_register_node` Register an rclcpp component with the ament resource index
* and create an executable.
* - `rclcpp_components_register_nodes` Register an rclcpp component with the ament resource index.
* The passed library can contain multiple nodes each registered via macro.
*/

#ifndef CARMA_ROS2_UTILS__LIFECYCLE_COMPONENT_WRAPPER_HPP__
#define CARMA_ROS2_UTILS__LIFECYCLE_COMPONENT_WRAPPER_HPP__

#include <map>
#include <unordered_map>
#include <tuple>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <boost/optional.hpp>

#include <composition_interfaces/srv/load_node.hpp>
#include <composition_interfaces/srv/unload_node.hpp>
#include <composition_interfaces/srv/list_nodes.hpp>

#include <rclcpp/rclcpp.hpp>
#include <rclcpp/executor.hpp>
#include <rclcpp/node_options.hpp>
#include <rclcpp_components/component_manager.hpp>

#include <rclcpp_components/node_factory.hpp>
#include <rclcpp_components/visibility_control.hpp>

#include "carma_ros2_utils/carma_lifecycle_node.hpp"

namespace class_loader
{
class ClassLoader;
} // namespace class_loader

namespace carma_ros2_utils
{

/// LifecycleComponentWrapper handles the services to load, unload, and get the list of loaded components.
class LifecycleComponentWrapper : public carma_ros2_utils::CarmaLifecycleNode
{
public:
using LoadNode = composition_interfaces::srv::LoadNode;
using UnloadNode = composition_interfaces::srv::UnloadNode;
using ListNodes = composition_interfaces::srv::ListNodes;

/// Represents a component resource.
/**
* Is a pair of class name (for class loader) and library path (absolute)
*/
using ComponentResource = std::pair<std::string, std::string>;

/// Default constructor
/**
* Initializes the component manager. It creates the services: load node, unload node
* and list nodes.
*
* \param node_options additional options to control creation of the node.
*/
RCLCPP_COMPONENTS_PUBLIC
LifecycleComponentWrapper(
const rclcpp::NodeOptions & node_options = rclcpp::NodeOptions()
.start_parameter_services(false)
.start_parameter_event_publisher(false));


/**
* \brief Method to initialize the component management services.
* NOTE: It is critical to call this method before .spin()
*
* The reason this method is not part of the constructor is to conform to the CarmaLifecycleNode API.
*
* \param executor the executor which will spin the node.
*/
RCLCPP_COMPONENTS_PUBLIC
void initialize(std::weak_ptr<rclcpp::Executor> executor);

RCLCPP_COMPONENTS_PUBLIC
virtual ~LifecycleComponentWrapper();

/// Return a list of valid loadable components in a given package.
/**
* \param package_name name of the package
* \param resource_index name of the executable
* \throws ComponentManagerException if the resource was not found or a invalid resource entry
* \return a list of component resources
*/
RCLCPP_COMPONENTS_PUBLIC
virtual std::vector<ComponentResource>
get_component_resources(
const std::string & package_name,
const std::string & resource_index = "rclcpp_components") const;

/// Instantiate a component from a dynamic library.
/**
* \param resource a component resource (class name + library path)
* \return a NodeFactory interface
*/
RCLCPP_COMPONENTS_PUBLIC
virtual std::shared_ptr<rclcpp_components::NodeFactory>
create_component_factory(const ComponentResource & resource);

protected:
/// Create node options for loaded component
/**
* \param request information with the node to load
* \return node options
*/
RCLCPP_COMPONENTS_PUBLIC
virtual rclcpp::NodeOptions
create_node_options(const std::shared_ptr<LoadNode::Request> request);

/// Service callback to load a new node in the component
/**
* This function allows to add parameters, remap rules, a specific node, name a namespace
* and/or additional arguments.
*
* \param request_header unused
* \param request information with the node to load
* \param response
*
* ///// CARMA CHANGE /////
* \param internal_call Optional parameter which if set means that the node should be loaded with the specified unique_id.
* This is used internally to track deferred node loading.
* ///// END CARMA CHANGE /////
*
* \throws std::overflow_error if node_id suffers an overflow. Very unlikely to happen at 1 kHz
* (very optimistic rate). it would take 585 years.
* \throws ComponentManagerException In the case that the component constructor throws an
* exception, rethrow into the following catch block.
*/
RCLCPP_COMPONENTS_PUBLIC
virtual void
on_load_node(
const std::shared_ptr<rmw_request_id_t> request_header,
const std::shared_ptr<LoadNode::Request> request,
std::shared_ptr<LoadNode::Response> response, boost::optional<uint64_t> internal_id = boost::none);

/**
* \deprecated Use on_load_node() instead
*/
[[deprecated("Use on_load_node() instead")]]
RCLCPP_COMPONENTS_PUBLIC
virtual void
OnLoadNode(
const std::shared_ptr<rmw_request_id_t> request_header,
const std::shared_ptr<LoadNode::Request> request,
std::shared_ptr<LoadNode::Response> response, boost::optional<uint64_t> internal_id = boost::none)
{
on_load_node(request_header, request, response, internal_id);
}

/// Service callback to unload a node in the component
/**
* \param request_header unused
* \param request unique identifier to remove from the component
* \param response true on the success field if the node unload was succefully, otherwise false
* and the error_message field contains the error.
*
*/
RCLCPP_COMPONENTS_PUBLIC
virtual void
on_unload_node(
const std::shared_ptr<rmw_request_id_t> request_header,
const std::shared_ptr<UnloadNode::Request> request,
std::shared_ptr<UnloadNode::Response> response);

/**
* \deprecated Use on_unload_node() instead
*/
[[deprecated("Use on_unload_node() instead")]]
RCLCPP_COMPONENTS_PUBLIC
virtual void
OnUnloadNode(
const std::shared_ptr<rmw_request_id_t> request_header,
const std::shared_ptr<UnloadNode::Request> request,
std::shared_ptr<UnloadNode::Response> response)
{
on_unload_node(request_header, request, response);
}

/// Service callback to get the list of nodes in the component
/**
* Return a two list: one with the unique identifiers and other with full name of the nodes.
*
* \param request_header unused
* \param request unused
* \param response list with the unique ids and full node names
*/
RCLCPP_COMPONENTS_PUBLIC
virtual void
on_list_nodes(
const std::shared_ptr<rmw_request_id_t> request_header,
const std::shared_ptr<ListNodes::Request> request,
std::shared_ptr<ListNodes::Response> response);

/**
* \deprecated Use on_list_nodes() instead
*/
[[deprecated("Use on_list_nodes() instead")]]
RCLCPP_COMPONENTS_PUBLIC
virtual void
OnListNodes(
const std::shared_ptr<rmw_request_id_t> request_header,
const std::shared_ptr<ListNodes::Request> request,
std::shared_ptr<ListNodes::Response> response)
{
on_list_nodes(request_header, request, response);
}

///// CARMA CHANGE /////

/**
* \brief Helper method to unload all the currently loaded nodes
*
* The primary unloading logic is propagated to the on_unload_node
*
* \return True if all nodes were successfully unloaded. False is otherwise
*/
bool unload_all_nodes();

///// Overrides /////
// handle_on_configure is not used
carma_ros2_utils::CallbackReturn handle_on_activate(const rclcpp_lifecycle::State &prev_state) override;
carma_ros2_utils::CallbackReturn handle_on_deactivate(const rclcpp_lifecycle::State &prev_state) override;
carma_ros2_utils::CallbackReturn handle_on_cleanup(const rclcpp_lifecycle::State &prev_state) override;
carma_ros2_utils::CallbackReturn handle_on_error(const rclcpp_lifecycle::State &prev_state, const std::string &exception_string) override;
carma_ros2_utils::CallbackReturn handle_on_shutdown(const rclcpp_lifecycle::State &prev_state) override;

///// END CARMA CHANGE /////

private:
std::weak_ptr<rclcpp::Executor> executor_;

uint64_t unique_id_ {1};
std::map<std::string, std::unique_ptr<class_loader::ClassLoader>> loaders_;
std::map<uint64_t, rclcpp_components::NodeInstanceWrapper> node_wrappers_;

rclcpp::Service<LoadNode>::SharedPtr loadNode_srv_;
rclcpp::Service<UnloadNode>::SharedPtr unloadNode_srv_;
rclcpp::Service<ListNodes>::SharedPtr listNodes_srv_;

///// CARMA CHANGE /////

//! A record of all the node load requests organized by their unique id
std::unordered_map<uint64_t, std::pair<rmw_request_id_t, LoadNode::Request>> load_node_requests_;

///// END CARMA CHANGE /////
};

} // namespace carma_ros2_utils

#endif // CARMA_ROS2_UTILS__LIFECYCLE_COMPONENT_WRAPPER_HPP__
9 changes: 7 additions & 2 deletions carma_ros2_utils/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,31 @@
<build_depend>libboost-program-options-dev</build_depend>
<build_depend>carma_cmake_common</build_depend>

<build_depend>rcpputils</build_depend>

<depend>launch</depend>
<depend>launch_testing_ament_cmake</depend>
<depend>lifecycle_msgs</depend>
<depend>rclcpp</depend>
<depend>rclcpp_lifecycle</depend>
<depend>rclcpp_components</depend>
<depend>rcutils</depend>
<depend>carma_msgs</depend>
<depend>ament_index_cpp</depend>
<depend>class_loader</depend>
<depend>composition_interfaces</depend>


<exec_depend>libboost-program-options</exec_depend>

<test_depend>ament_lint_common</test_depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_cmake_gtest</test_depend>
<test_depend>launch</test_depend>
<test_depend>launch_testing_ament_cmake</test_depend>
<test_depend>launch_testing_ros</test_depend>
<test_depend>ament_cmake_pytest</test_depend>
<test_depend>ros2_lifecycle_manager</test_depend>
<test_depend>std_srvs</test_depend>
<test_depend>launch_testing</test_depend>

<export>
<build_type>ament_cmake</build_type>
Expand Down
49 changes: 36 additions & 13 deletions carma_ros2_utils/src/carma_lifecycle_node.cpp
Original file line number Diff line number Diff line change
@@ -1,16 +1,39 @@
// Copyright 2021 Open Source Robotics Foundation, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/*
* Copyright (C) 2021 LEIDOS.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

/**
* This file is loosely based on the reference architecture developed by OSRF for Leidos located here
* https://github.com/mjeronimo/carma2/blob/master/carma_utils/carma_utils/src/carma_lifecycle_node.cpp
*
* That file has the following license and some code snippets from it may be present in this file as well and are under the same license.
*
* Copyright 2021 Open Source Robotics Foundation, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

#include "carma_ros2_utils/carma_lifecycle_node.hpp"

Expand Down
410 changes: 410 additions & 0 deletions carma_ros2_utils/src/lifecycle_component_wrapper.cpp

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions carma_ros2_utils/src/lifecycle_component_wrapper_mt.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2019 Open Source Robotics Foundation, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
* Modifications copyright (C) 2021 Leidos
* - Converted to use Lifecycle Component Wrapper
*
*/

#include <memory>

#include <rclcpp/rclcpp.hpp>

#include "carma_ros2_utils/lifecycle_component_wrapper.hpp"

int main(int argc, char * argv[])
{
/// Component container with a multi-threaded executor.
rclcpp::init(argc, argv);
auto exec = std::make_shared<rclcpp::executors::MultiThreadedExecutor>();
auto node = std::make_shared<carma_ros2_utils::LifecycleComponentWrapper>();
node->initialize(exec);
exec->add_node(node->get_node_base_interface());
exec->spin();
}
36 changes: 36 additions & 0 deletions carma_ros2_utils/src/lifecycle_component_wrapper_st.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2019 Open Source Robotics Foundation, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
* Modifications copyright (C) 2021 Leidos
* - Converted to use Lifecycle Component Wrapper
*
*/

#include <memory>

#include "rclcpp/rclcpp.hpp"

#include "carma_ros2_utils/lifecycle_component_wrapper.hpp"

int main(int argc, char * argv[])
{
/// Component container with a single-threaded executor.
rclcpp::init(argc, argv);
auto exec = std::make_shared<rclcpp::executors::SingleThreadedExecutor>();
auto node = std::make_shared<carma_ros2_utils::LifecycleComponentWrapper>();
node->initialize(exec);
exec->add_node(node->get_node_base_interface());
exec->spin();
}
28 changes: 24 additions & 4 deletions carma_ros2_utils/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ find_package(ament_cmake_gtest REQUIRED)
find_package(ament_cmake_pytest REQUIRED)
find_package(ros2_lifecycle_manager REQUIRED)
find_package(std_srvs REQUIRED)
find_package(rclcpp_components REQUIRED)
find_package(ament_cmake_pytest REQUIRED)
find_package(launch_testing_ament_cmake REQUIRED)

set(dependencies ${dependencies} ros2_lifecycle_manager std_srvs)

Expand All @@ -20,6 +23,19 @@ target_include_directories(test_carma_lifecycle_node
$<INSTALL_INTERFACE:include>
)

add_library(test_minimal_node SHARED
test_minimal_node.cpp
)

ament_target_dependencies(test_minimal_node
rclcpp
rclcpp_components
)

add_launch_test(
launch_test_lifecycle_wrapper.py
)


ament_add_gtest_executable(test_lifecycle_gtest
test_lifecycle_manager.cpp
Expand Down Expand Up @@ -61,10 +77,14 @@ ament_target_dependencies(test_carma_lifecycle_gtest
)

install(
TARGETS test_lifecycle_gtest test_carma_lifecycle_node test_carma_lifecycle_gtest
TARGETS test_lifecycle_gtest test_carma_lifecycle_node test_carma_lifecycle_gtest test_minimal_node
# EXPORT should not be needed for unit tests
LIBRARY DESTINATION lib/${PROJECT_NAME} # In Ament, Executables are installed to lib/${PROJECT_NAME} not bin
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib/${PROJECT_NAME}
RUNTIME DESTINATION lib/${PROJECT_NAME}
RUNTIME DESTINATION lib/${PROJECT_NAME} # In Ament, Executables are installed to lib/${PROJECT_NAME} not bin
INCLUDES DESTINATION include
)
)

# This is a bit of a hack to allow for rclcpp_component registration in this subdirectory
# Based off sloretz's answer from ROS Answers here https://answers.ros.org/question/361289/ros2-components-registration-from-subdirectory-cmakelists-file/
#set(AMENT_EXTENSIONS_ament_package ${AMENT_EXTENSIONS_ament_package} PARENT_SCOPE)
119 changes: 119 additions & 0 deletions carma_ros2_utils/test/launch_test_lifecycle_wrapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#! /usr/bin/env python3
#
# Copyright (C) 2021 LEIDOS.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may not
# use this file except in compliance with the License. You may obtain a copy of
# the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations under
# the License.
#

import os
import sys

import launch
import launch_ros
from launch import LaunchDescription
from launch import LaunchService
from launch.actions import ExecuteProcess
from launch_ros.actions import ComposableNodeContainer
from launch_ros.descriptions import ComposableNode
import launch_testing
import pytest
import unittest

import rclpy
from rclpy.node import Node

from carma_msgs.msg import SystemAlert
from lifecycle_msgs.srv import GetState
import time
import os, subprocess

@pytest.mark.launch_test
def generate_test_description():

ld = launch.LaunchDescription([
ComposableNodeContainer(
package='carma_ros2_utils',
name='wrapper_container',
executable='lifecycle_component_wrapper_st',
namespace="/",
exec_name='wrapper_container',
composable_node_descriptions=[

ComposableNode(
package='carma_ros2_utils',
plugin='carma_ros2_utils_testing::MinimalNode',
name='minimal_node'
),
]
),

# We can start this test right away as there is nothing else to wait on
launch_testing.actions.ReadyToTest()
])

return ld



# All tests in this class will run in parallel
class TestRuntime(unittest.TestCase):

def test_nominal_lifecycle(self, proc_output):

# Check that that the wrapper has started and did not load any nodes on startup
proc_output.assertWaitFor("Got request to load node: minimal_node but we are not in the active state, caching for later lifecycle based activation.", process='wrapper_container', strict_proc_matching=True,timeout=5)


d = dict(os.environ) # Make a copy of the current environment
completed_proc = subprocess.run(['ros2', 'lifecycle', 'set', '/wrapper_container', 'configure'], env=d, capture_output=True, timeout=8)

output_str = completed_proc.stdout.decode()
self.assertTrue( output_str == 'Transitioning successful\n') # For whatever reason == must be specified manually instead of calling assertEquals which does not play well with the decoded string

completed_proc = subprocess.run(['ros2', 'lifecycle', 'set', '/wrapper_container', 'activate'], env=d, capture_output=True, timeout=8)

output_str = completed_proc.stdout.decode()
self.assertTrue(output_str == 'Transitioning successful\n')

proc_output.assertWaitFor("libtest_minimal_node.so", process='wrapper_container', strict_proc_matching=True,timeout=5)
proc_output.assertWaitFor("Found class: rclcpp_components::NodeFactoryTemplate<carma_ros2_utils_testing::MinimalNode>", process='wrapper_container', strict_proc_matching=True,timeout=5)
proc_output.assertWaitFor("Instantiate class: rclcpp_components::NodeFactoryTemplate<carma_ros2_utils_testing::MinimalNode>", process='wrapper_container', strict_proc_matching=True,timeout=5)

completed_proc = subprocess.run(['ros2', 'lifecycle', 'set', '/wrapper_container', 'deactivate'], env=d, capture_output=True, timeout=8)

output_str = completed_proc.stdout.decode()
self.assertTrue(output_str == 'Transitioning successful\n')

completed_proc = subprocess.run(['ros2', 'lifecycle', 'set', '/wrapper_container', 'shutdown'], env=d, capture_output=True, timeout=8)

output_str = completed_proc.stdout.decode()
self.assertTrue(output_str == 'Transitioning successful\n')

completed_proc = subprocess.run(['ros2', 'lifecycle', 'get', '/wrapper_container'], env=d, capture_output=True, timeout=8)

output_str = completed_proc.stdout.decode()
self.assertTrue(output_str == 'finalized [4]\n') # Verify nodes are shutdown


# All tests in this class will run on shutdown
@launch_testing.post_shutdown_test()
class TestPostShutdown(unittest.TestCase):

def test_exit_code(self, proc_info):
# Check that all processes in the launch (in this case, there's just one) exit
# with code 0
launch_testing.asserts.assertExitCodes(proc_info, allowable_exit_codes=[0])




35 changes: 35 additions & 0 deletions carma_ros2_utils/test/test_minimal_node.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright (C) 2021 LEIDOS.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/

#include "rclcpp/rclcpp.hpp"

namespace carma_ros2_utils_testing {

// No-Op node for testing the lifecycle_component_wrapper
class MinimalNode : public rclcpp::Node
{
public:
MinimalNode(const rclcpp::NodeOptions&)
: Node("minimal_node") {}

};

} // carma_ros2_utils_testing

#include "rclcpp_components/register_node_macro.hpp"

// Register the component with class_loader
RCLCPP_COMPONENTS_REGISTER_NODE(carma_ros2_utils_testing::MinimalNode)