diff --git a/bazel_ros2_rules/ros2/BUILD.bazel b/bazel_ros2_rules/ros2/BUILD.bazel index c6697127..354a7bdf 100644 --- a/bazel_ros2_rules/ros2/BUILD.bazel +++ b/bazel_ros2_rules/ros2/BUILD.bazel @@ -22,6 +22,17 @@ py_library( deps = ["@bazel_tools//tools/python/runfiles"], ) +py_library( + name = "roslaunch_util.py", + srcs = [ + "tools/roslaunch_util/__init__.py", + "tools/roslaunch_util/roslaunch_util.py", + "tools/roslaunch_util/setup.py", + ], + visibility = ["//visibility:public"], + deps = ["@bazel_tools//tools/python/runfiles"], +) + run_calculate_rosidl_capitalization_tests() py_library( diff --git a/bazel_ros2_rules/ros2/ros_py.bzl b/bazel_ros2_rules/ros2/ros_py.bzl index 5184c8f3..81bd949f 100644 --- a/bazel_ros2_rules/ros2/ros_py.bzl +++ b/bazel_ros2_rules/ros2/ros_py.bzl @@ -130,6 +130,26 @@ def ros_py_binary( ) py_binary_rule(name = name, **kwargs) +def ros_launch( + name, + launch_file = None, + node_targets = []): + deps = ["@ros2//:ros2", "@bazel_ros2_rules//ros2:roslaunch_util.py"] + srcs = ["@bazel_ros2_rules//ros2:roslaunch_util.py"] + + data = [launch_file] + data += node_targets + args = [launch_file] + + ros_py_binary( + name = name, + main = "@bazel_ros2_rules//ros2:roslaunch_util.py", + deps = deps, + srcs = srcs, + data = data, + args = args, + ) + def ros_py_test( name, rmw_implementation = None, diff --git a/bazel_ros2_rules/ros2/tools/roslaunch_util/__init__.py b/bazel_ros2_rules/ros2/tools/roslaunch_util/__init__.py new file mode 100644 index 00000000..12a7db48 --- /dev/null +++ b/bazel_ros2_rules/ros2/tools/roslaunch_util/__init__.py @@ -0,0 +1,3 @@ +from .roslaunch_util import ExecuteBazelTarget + +__all__ = ['ExecuteBazelTarget'] diff --git a/bazel_ros2_rules/ros2/tools/roslaunch_util/roslaunch_util.py b/bazel_ros2_rules/ros2/tools/roslaunch_util/roslaunch_util.py new file mode 100644 index 00000000..dd02c0a2 --- /dev/null +++ b/bazel_ros2_rules/ros2/tools/roslaunch_util/roslaunch_util.py @@ -0,0 +1,52 @@ +import subprocess +import sys +import os + +from launch.actions import ExecuteProcess +from launch.frontend import expose_action + +# This util file serves as a wrapper over the cmdline +# way to run launch, using "ros2 launch launch_file.py". +# The ros_launch() bazel rule starts this script, which +# gets run as a ros_py_binary(). This way we get the ros +# environment set up for us for free. + +def find_rel_path(name, path): + for root, _, files in os.walk(path): + if name in files: + return os.path.relpath(os.path.join(root, name)) + +# Launch action to wrap over ExecuteProcess. +@expose_action('execute_bazel_target') +class ExecuteBazelTarget(ExecuteProcess): + def __init__(self,target,**kwargs): + super().__init__(cmd=[find_rel_path(target, os.getcwd())], + **kwargs) + + @classmethod + def parse(cls, entity, parser): + _, kwargs = super().parse(entity, parser, ignore=['cmd']) + kwargs['target'] = entity.get_attr('target') + return cls, kwargs + + def execute(self, context): + return super().execute(context) + +if __name__ == '__main__': + # TODO (Aditya): How do I stop installing this every time ? + # Required to get xml launch working, as we need to register an entry + # point for launch extensions. + os.system("pip install ../bazel_ros2_rules/ros2/tools/roslaunch_util >/dev/null 2>&1") + # Actual launch files should be able to import the custom action now. + env = {**os.environ, 'PYTHONPATH': os.getcwd() + '/external/bazel_ros2_rules/ros2/tools:' + + os.environ['PYTHONPATH']} + + launch_file_name = sys.argv[1] + + roslaunch_cli = "./external/ros2/ros2" + action = "launch" + # TODO : Is there a better way to locate the launch file exactly ? + launch_file = find_rel_path(launch_file_name, os.getcwd()) + + # This basically runs "ros2 launch launch_file" + subprocess.run([roslaunch_cli, action, launch_file], env=env) diff --git a/bazel_ros2_rules/ros2/tools/roslaunch_util/setup.py b/bazel_ros2_rules/ros2/tools/roslaunch_util/setup.py new file mode 100644 index 00000000..9c0ccfbe --- /dev/null +++ b/bazel_ros2_rules/ros2/tools/roslaunch_util/setup.py @@ -0,0 +1,10 @@ +from setuptools import setup + +setup( + name = "roslaunch_util", + entry_points={ + 'launch.frontend.launch_extension': [ + 'roslaunch_util = roslaunch_util', + ], + } +) diff --git a/ros2_example_bazel_installed/ros2_example_apps/BUILD.bazel b/ros2_example_bazel_installed/ros2_example_apps/BUILD.bazel index 92183db5..d15b7d63 100644 --- a/ros2_example_bazel_installed/ros2_example_apps/BUILD.bazel +++ b/ros2_example_bazel_installed/ros2_example_apps/BUILD.bazel @@ -4,6 +4,7 @@ load("@ros2//:ros_cc.bzl", "ros_cc_binary") load("@ros2//:ros_cc.bzl", "ros_cc_test") load("@ros2//:ros_py.bzl", "ros_py_binary", "ros_py_test") +load("@ros2//:ros_py.bzl", "ros_launch") load("@ros2//:rosidl.bzl", "rosidl_interfaces_group") load("//tools:cmd_test.bzl", "cmd_test") @@ -202,6 +203,45 @@ ros_py_binary( ], ) +# ROS launch example. +ros_py_binary( + name = "eg_talker", + srcs = ["roslaunch_eg_nodes/eg_talker.py"], + deps = [ + "@ros2//:rclpy_py", + "@ros2//:std_msgs_py", + ], +) + +ros_cc_binary( + name = "eg_listener", + srcs = ["roslaunch_eg_nodes/eg_listener.cpp"], + deps = [ + "@ros2//:rclcpp_cc", + "@ros2//:std_msgs_cc", + ], +) + +# Uses a python launch file to spawn the talker and listener. +ros_launch( + name = "roslaunch_eg_py", + launch_file = "eg_launch.py", + node_targets = [ + ":eg_talker", + ":eg_listener", + ], +) + +# Uses an xml launch file to spawn the talker and listener. +ros_launch( + name = "roslaunch_eg_xml", + launch_file = "eg_launch.xml", + node_targets = [ + ":eg_talker", + ":eg_listener", + ], +) + ros_py_test( name = "custom_message_rosbag_test", srcs = ["test/custom_message_rosbag_test.py"], diff --git a/ros2_example_bazel_installed/ros2_example_apps/eg_launch.py b/ros2_example_bazel_installed/ros2_example_apps/eg_launch.py new file mode 100644 index 00000000..d2ed52b3 --- /dev/null +++ b/ros2_example_bazel_installed/ros2_example_apps/eg_launch.py @@ -0,0 +1,10 @@ +from launch import LaunchDescription +from roslaunch_util import ExecuteBazelTarget + +def generate_launch_description(): + return LaunchDescription([ + # Running a talker written in python. + ExecuteBazelTarget('eg_talker'), + # Running a listener written in cpp. + ExecuteBazelTarget('eg_listener'), + ]) diff --git a/ros2_example_bazel_installed/ros2_example_apps/eg_launch.xml b/ros2_example_bazel_installed/ros2_example_apps/eg_launch.xml new file mode 100644 index 00000000..07f1a926 --- /dev/null +++ b/ros2_example_bazel_installed/ros2_example_apps/eg_launch.xml @@ -0,0 +1,4 @@ + + + + diff --git a/ros2_example_bazel_installed/ros2_example_apps/roslaunch_eg_nodes/eg_listener.cpp b/ros2_example_bazel_installed/ros2_example_apps/roslaunch_eg_nodes/eg_listener.cpp new file mode 100644 index 00000000..733fcac2 --- /dev/null +++ b/ros2_example_bazel_installed/ros2_example_apps/roslaunch_eg_nodes/eg_listener.cpp @@ -0,0 +1,31 @@ +#include + +#include "rclcpp/rclcpp.hpp" +#include "std_msgs/msg/string.hpp" +using std::placeholders::_1; + +class MinimalSubscriber : public rclcpp::Node +{ + public: + MinimalSubscriber() + : Node("minimal_subscriber") + { + subscription_ = this->create_subscription( + "topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1)); + } + + private: + void topic_callback(const std_msgs::msg::String::SharedPtr msg) const + { + RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str()); + } + rclcpp::Subscription::SharedPtr subscription_; +}; + +int main(int argc, char * argv[]) +{ + rclcpp::init(argc, argv); + rclcpp::spin(std::make_shared()); + rclcpp::shutdown(); + return 0; +} diff --git a/ros2_example_bazel_installed/ros2_example_apps/roslaunch_eg_nodes/eg_talker.py b/ros2_example_bazel_installed/ros2_example_apps/roslaunch_eg_nodes/eg_talker.py new file mode 100644 index 00000000..4b3194bb --- /dev/null +++ b/ros2_example_bazel_installed/ros2_example_apps/roslaunch_eg_nodes/eg_talker.py @@ -0,0 +1,39 @@ +import rclpy +from rclpy.node import Node + +from std_msgs.msg import String + + +class MinimalPublisher(Node): + + def __init__(self): + super().__init__('minimal_publisher') + self.publisher_ = self.create_publisher(String, 'topic', 10) + timer_period = 0.5 # seconds + self.timer = self.create_timer(timer_period, self.timer_callback) + self.i = 0 + + def timer_callback(self): + msg = String() + msg.data = 'Hello World: %d' % self.i + self.publisher_.publish(msg) + self.get_logger().info('Publishing: "%s"' % msg.data) + self.i += 1 + + +def main(args=None): + rclpy.init(args=args) + + minimal_publisher = MinimalPublisher() + + rclpy.spin(minimal_publisher) + + # Destroy the node explicitly + # (optional - otherwise it will be done automatically + # when the garbage collector destroys the node object) + minimal_publisher.destroy_node() + rclpy.shutdown() + + +if __name__ == '__main__': + main()