Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions isaac_ros_managed_nitros/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,30 @@ if(BUILD_TESTING)
set(ament_cmake_copyright_FOUND TRUE)
ament_lint_auto_find_test_dependencies()

# NitrosEmptyLifecycleNode — validates ManagedNitrosPublisher with LifecycleNode (issue #68)
find_package(rclcpp_lifecycle REQUIRED)
find_package(launch_testing_ament_cmake REQUIRED)

add_library(nitros_empty_lifecycle_node SHARED
test/src/nitros_empty_lifecycle_node.cpp)
target_include_directories(nitros_empty_lifecycle_node PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include)
ament_target_dependencies(nitros_empty_lifecycle_node
isaac_ros_nitros
rclcpp_lifecycle
rclcpp_components)
set_target_properties(nitros_empty_lifecycle_node PROPERTIES
BUILD_WITH_INSTALL_RPATH TRUE
BUILD_RPATH_USE_ORIGIN TRUE
INSTALL_RPATH_USE_LINK_PATH TRUE)
rclcpp_components_register_nodes(
nitros_empty_lifecycle_node
"nvidia::isaac_ros::nitros::NitrosEmptyLifecycleNode")
install(TARGETS nitros_empty_lifecycle_node
DESTINATION lib)

add_launch_test(test/isaac_ros_nitros_lifecycle_test_pol.py TIMEOUT "30")

endif()


Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES
// Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -33,6 +33,7 @@
#include "extensions/gxf_optimizer/core/optimizer.hpp"
#include "extensions/gxf_optimizer/exporter/graph_types.hpp"

#include "isaac_ros_nitros/nitros_node_interfaces.hpp"
#include "isaac_ros_nitros/nitros_publisher.hpp"
#include "isaac_ros_nitros/types/nitros_type_manager.hpp"
#include "isaac_ros_nitros/nitros_publisher_subscriber_group.hpp"
Expand All @@ -51,15 +52,17 @@ template<typename T>
class ManagedNitrosPublisher
{
public:
template<typename NodeT>
ManagedNitrosPublisher(
rclcpp::Node * node,
NodeT * node,
const std::string & topic,
const std::string & format,
const NitrosDiagnosticsConfig & diagnostics_config = {},
const rclcpp::QoS qos = rclcpp::QoS(1))
: node_{node},
: node_ifaces_(MakeNitrosNodeInterfaces(*node)),
context_{GetTypeAdapterNitrosContext()},
nitros_type_manager_{std::make_shared<NitrosTypeManager>(node_)}
nitros_type_manager_{std::make_shared<NitrosTypeManager>(
node_ifaces_.get<rclcpp::node_interfaces::NodeLoggingInterface>()->get_logger())}
{
nitros_type_manager_->registerSupportedType<T>();
nitros_type_manager_->loadExtensions(format);
Expand All @@ -74,13 +77,14 @@ class ManagedNitrosPublisher
};

nitros_pub_ = std::make_shared<NitrosPublisher>(
*node_, GetTypeAdapterNitrosContext().getContext(), nitros_type_manager_,
node_ifaces_, GetTypeAdapterNitrosContext().getContext(), nitros_type_manager_,
supported_data_formats, component_config, diagnostics_config);

nitros_pub_->start();

RCLCPP_INFO(
node_->get_logger().get_child("ManagedNitrosPublisher"),
node_ifaces_.get<rclcpp::node_interfaces::NodeLoggingInterface>()->get_logger().get_child(
"ManagedNitrosPublisher"),
"Starting Managed Nitros Publisher");
}

Expand All @@ -90,7 +94,7 @@ class ManagedNitrosPublisher
}

private:
rclcpp::Node * node_;
NitrosNodeInterfaces node_ifaces_;
NitrosContext context_;
std::shared_ptr<NitrosTypeManager> nitros_type_manager_;
std::shared_ptr<NitrosPublisher> nitros_pub_;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES
// Copyright (c) 2022-2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// Copyright (c) 2022-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -23,6 +23,7 @@
#include <vector>

#include "extensions/gxf_optimizer/core/optimizer.hpp"
#include "isaac_ros_nitros/nitros_node_interfaces.hpp"
#include "isaac_ros_nitros/nitros_subscriber.hpp"
#include "isaac_ros_nitros/types/nitros_type_manager.hpp"
#include "rclcpp/rclcpp.hpp"
Expand All @@ -39,15 +40,17 @@ template<typename NitrosMsgView>
class ManagedNitrosSubscriber
{
public:
template<typename NodeT>
explicit ManagedNitrosSubscriber(
rclcpp::Node * node,
NodeT * node,
const std::string & topic_name,
const std::string & format,
std::function<void(const NitrosMsgView & msg_view)> callback = nullptr,
const NitrosDiagnosticsConfig & diagnostics_config = {},
const rclcpp::QoS qos = rclcpp::QoS(1))
: node_{node}, topic_{topic_name},
nitros_type_manager_{std::make_shared<NitrosTypeManager>(node_)}
: node_ifaces_(MakeNitrosNodeInterfaces(*node)), topic_{topic_name},
nitros_type_manager_{std::make_shared<NitrosTypeManager>(
node_ifaces_.get<rclcpp::node_interfaces::NodeLoggingInterface>()->get_logger())}
{
nitros_type_manager_->registerSupportedType<typename NitrosMsgView::BaseType>();
nitros_type_manager_->loadExtensions(format);
Expand All @@ -60,24 +63,27 @@ class ManagedNitrosSubscriber
.compatible_data_format = format,
.topic_name = topic_name,
.callback = [callback](const gxf_context_t, NitrosTypeBase & msg) -> void {
const NitrosMsgView view(*(static_cast<typename NitrosMsgView::BaseType *>(&msg)));
callback(view);
if (callback) {
const NitrosMsgView view(*(static_cast<typename NitrosMsgView::BaseType *>(&msg)));
callback(view);
}
}
};

nitros_sub_ = std::make_shared<NitrosSubscriber>(
*node_, GetTypeAdapterNitrosContext().getContext(), nitros_type_manager_,
node_ifaces_, GetTypeAdapterNitrosContext().getContext(), nitros_type_manager_,
supported_data_formats, component_config, diagnostics_config);

nitros_sub_->start();

RCLCPP_INFO(
node_->get_logger().get_child("ManagedNitrosSubscriber"),
node_ifaces_.get<rclcpp::node_interfaces::NodeLoggingInterface>()->get_logger().get_child(
"ManagedNitrosSubscriber"),
"Starting Managed Nitros Subscriber");
}

private:
rclcpp::Node * node_;
NitrosNodeInterfaces node_ifaces_;
std::string topic_;
std::shared_ptr<NitrosTypeManager> nitros_type_manager_;
std::shared_ptr<NitrosSubscriber> nitros_sub_;
Expand Down
3 changes: 3 additions & 0 deletions isaac_ros_managed_nitros/package.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ license agreement from NVIDIA CORPORATION is strictly prohibited.
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<test_depend>isaac_ros_test</test_depend>
<test_depend>rclcpp_lifecycle</test_depend>
<test_depend>launch_testing_ament_cmake</test_depend>
<test_depend>lifecycle_msgs</test_depend>

<export>
<build_type>ament_cmake</build_type>
Expand Down
147 changes: 147 additions & 0 deletions isaac_ros_managed_nitros/test/isaac_ros_nitros_lifecycle_test_pol.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# SPDX-FileCopyrightText: NVIDIA CORPORATION & AFFILIATES
# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
#
# 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.
#
# SPDX-License-Identifier: Apache-2.0

"""
Launch test verifying that ManagedNitrosPublisher/Subscriber work inside a LifecycleNode.

The test loads NitrosEmptyLifecycleNode in a component container and drives it through
the full lifecycle state machine:

configure → activate → deactivate → cleanup → configure → shutdown

This exercises every on_*() callback and verifies that ManagedNitrosPublisher and
ManagedNitrosSubscriber are constructed, used, and destroyed correctly at each stage.
"""

import time

from isaac_ros_test import IsaacROSBaseTest
import launch
from launch_ros.actions import ComposableNodeContainer
from launch_ros.descriptions import ComposableNode
import launch_testing
from lifecycle_msgs.msg import Transition
from lifecycle_msgs.srv import ChangeState
import pytest
import rclpy


@pytest.mark.rostest
def generate_test_description():
"""Generate launch description with NitrosEmptyLifecycleNode."""
container = ComposableNodeContainer(
name='nitros_lifecycle_container',
namespace='',
package='rclcpp_components',
executable='component_container_mt',
composable_node_descriptions=[
ComposableNode(
package='isaac_ros_managed_nitros',
plugin='nvidia::isaac_ros::nitros::NitrosEmptyLifecycleNode',
name='nitros_empty_lifecycle_node',
),
],
output='both',
)

return TestNitrosLifecycle.generate_test_description([
container,
launch.actions.TimerAction(
period=2.5, actions=[launch_testing.actions.ReadyToTest()])
])


class TestNitrosLifecycle(IsaacROSBaseTest):
"""Test the full lifecycle state machine of NitrosEmptyLifecycleNode."""

package = 'isaac_ros_managed_nitros'

def _make_change_state_client(self):
"""Return a ChangeState client for the lifecycle node."""
return self.node.create_client(
ChangeState,
'/nitros_empty_lifecycle_node/change_state',
)

def _send_transition(self, client, transition_id, timeout_sec=10.0):
"""Send a lifecycle transition and return the result, or None on timeout."""
available = client.wait_for_service(timeout_sec=timeout_sec)
if not available:
return None
request = ChangeState.Request()
request.transition.id = transition_id
future = client.call_async(request)
rclpy.spin_until_future_complete(self.node, future, timeout_sec=timeout_sec)
return future.result()

def test_lifecycle_full_sequence(self):
"""
Drive the complete lifecycle state machine in one ordered test.

A single test method avoids cross-test state leakage (all test methods share
the same launched node; leaving the node in a non-unconfigured state would
make subsequent transitions invalid).

Sequence:
1. configure (unconfigured → inactive): constructs ManagedNitrosPublisher
and ManagedNitrosSubscriber
2. activate (inactive → active): exercises on_activate()
3. deactivate (active → inactive): exercises on_deactivate()
4. cleanup (inactive → unconfigured): destroys pub/sub, verifies
on_cleanup() returns SUCCESS
5. configure (unconfigured → inactive): re-constructs pub/sub, verifies
resources can be re-created after a cleanup
6. shutdown (inactive → finalized): destroys pub/sub via on_shutdown(),
verifies clean teardown from the inactive state
"""
client = self._make_change_state_client()

start = time.time()
self.assertTrue(
client.wait_for_service(timeout_sec=10.0),
msg=f'ChangeState service not available after {time.time() - start:.1f}s',
)

# Step 1: configure — creates publisher + subscriber
result = self._send_transition(client, Transition.TRANSITION_CONFIGURE)
self.assertIsNotNone(result, 'configure (1st) future returned None')
self.assertTrue(result.success, 'configure (1st) transition failed')

# Step 2: activate — exercises on_activate()
result = self._send_transition(client, Transition.TRANSITION_ACTIVATE)
self.assertIsNotNone(result, 'activate future returned None')
self.assertTrue(result.success, 'activate transition failed')

# Step 3: deactivate — exercises on_deactivate()
result = self._send_transition(client, Transition.TRANSITION_DEACTIVATE)
self.assertIsNotNone(result, 'deactivate future returned None')
self.assertTrue(result.success, 'deactivate transition failed')

# Step 4: cleanup — destroys publisher + subscriber
result = self._send_transition(client, Transition.TRANSITION_CLEANUP)
self.assertIsNotNone(result, 'cleanup future returned None')
self.assertTrue(result.success, 'cleanup transition failed')

# Step 5: configure again — verifies resources can be re-created after cleanup
result = self._send_transition(client, Transition.TRANSITION_CONFIGURE)
self.assertIsNotNone(result, 'configure (2nd) future returned None')
self.assertTrue(result.success, 'configure (2nd) transition failed')

# Step 6: shutdown — destroys pub/sub via on_shutdown(), node enters finalized
result = self._send_transition(client, Transition.TRANSITION_INACTIVE_SHUTDOWN)
self.assertIsNotNone(result, 'shutdown future returned None')
self.assertTrue(result.success, 'shutdown transition failed')
Loading