Skip to content

Commit 4844894

Browse files
authored
Script to create a template package (#10)
* working bash script to create a new package via ros2 pkg create - inside the parent module's container terminal * boilerplate files * readme on usage and description
1 parent 0277d84 commit 4844894

File tree

10 files changed

+415
-0
lines changed

10 files changed

+415
-0
lines changed

utils/README.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Utils
2+
3+
## `create-package.bash`
4+
5+
### Usage
6+
`create-package.bash $package_name $module_name [-p] [-h]`
7+
- $package_name -> required, str: name of package
8+
- $module_name -> required, str: name of parent module
9+
- -p -> optional, flag: build python package. Defaults to a cpp package.
10+
- -h -> optional, flag: help description.
11+
12+
- ⚠️ Double check for these before running the script:
13+
- The `module_name` directory must exist beforehand for the script to work properly.
14+
- The `modules/docker-compose.module_name.yaml` must exist for the script to run.
15+
16+
### Description
17+
- Creates a ros2 python or cpp `package_name` in the `module_name`
18+
- Creates missing boilerplate files
19+
- If CPP build, these are:
20+
- launch/`package_name`.launch.py
21+
- config/params.yaml
22+
- src/`package_name`_node.cpp
23+
- include/`package_name`_node.hpp
24+
- src/`package_name`_core.cpp
25+
- include/`package_name`_core.hpp
26+
- test/`package_name`_test.cpp
27+
- If Python build, these are:
28+
- launch/`package_name`.launch.py
29+
- config/params.yaml
30+
- `package_name`/`package_name`_node.py
31+
- `package_name`/`package_name`_core.py
32+
- Modifies module_name.interfacing.Dockerfile to copy in package code into container.
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import os
2+
from ament_index_python.packages import get_package_share_directory
3+
from launch import LaunchDescription
4+
from launch.actions import DeclareLaunchArgument, OpaqueFunction
5+
from launch.substitutions import LaunchConfiguration
6+
from launch_ros.actions import Node
7+
8+
9+
def launch_setup(context, *args, **kwargs):
10+
"""
11+
Dynamically determine package name and configuration path.
12+
13+
This function is called by OpaqueFunction to resolve launch arguments
14+
and create the Node action.
15+
"""
16+
# These values are resolved from the DeclareLaunchArgument definitions below
17+
package_name = LaunchConfiguration('package_name').perform(context)
18+
executable_name = LaunchConfiguration('executable_name').perform(context)
19+
node_name = LaunchConfiguration('node_name').perform(context)
20+
21+
# Construct the path to the parameter file within the current package
22+
# This assumes your params.yaml is in a 'config' subdirectory within your package's share directory.
23+
param_file_path = os.path.join(
24+
get_package_share_directory(package_name),
25+
'config',
26+
'params.yaml' # <-- REPLACE if your parameter file has a different name
27+
)
28+
29+
# Check if the params.yaml file actually exists
30+
node_parameters = []
31+
if os.path.exists(param_file_path):
32+
node_parameters.append(param_file_path)
33+
print(f"INFO: Using parameter file for {node_name}: {param_file_path}")
34+
else:
35+
print(f"WARNING: No params.yaml found for {node_name} at {param_file_path}. Launching without parameters.")
36+
37+
38+
# Define the Node action
39+
node = Node(
40+
package=package_name,
41+
executable=executable_name,
42+
name=node_name,
43+
parameters=node_parameters,
44+
output='screen', # Optional: 'screen' or 'log'. 'screen' prints output to the console.
45+
emulate_tty=True, # Optional: Set to True for colored output in the console.
46+
)
47+
48+
return [node]
49+
50+
51+
def generate_launch_description():
52+
"""
53+
Generate the launch description for a generic ROS 2 package.
54+
55+
This template is designed to be placed in any ROS 2 package's
56+
launch directory. It expects the package to have a main executable
57+
and optionally a 'config/params.yaml' file.
58+
"""
59+
return LaunchDescription([
60+
# Declare the package name argument.
61+
# This argument specifies WHICH ROS 2 package this launch file should target.
62+
# When running, you will set this:
63+
# e.g., ros2 launch <path_to_this_launch_file> generic_package.launch.py package_name:=your_actual_package_name
64+
DeclareLaunchArgument(
65+
'package_name',
66+
description='Name of the ROS 2 package to launch.'
67+
),
68+
# Declare the executable name argument.
69+
# This argument specifies WHICH executable within the 'package_name' should be run.
70+
# When running, you will set this:
71+
# e.g., ros2 launch ... executable_name:=your_node_executable_name
72+
DeclareLaunchArgument(
73+
'executable_name',
74+
description='Name of the executable to run from the package.'
75+
),
76+
# Declare the node name argument (optional, defaults to executable_name)
77+
# This argument sets the ROS 2 node name. If not provided, it defaults to the executable name.
78+
# e.g., ros2 launch ... node_name:=my_custom_node_name
79+
DeclareLaunchArgument(
80+
'node_name',
81+
default_value=LaunchConfiguration('executable_name'), # Defaults to the executable name
82+
description='Name to assign to the ROS 2 node.'
83+
),
84+
# OpaqueFunction defers the creation of the Node action until launch arguments are resolved.
85+
# This is necessary because we need the actual string values of package_name, executable_name, etc.
86+
OpaqueFunction(function=launch_setup)
87+
])
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#include "foo_core.hpp"
2+
3+
FooCore::FooCore() {
4+
5+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#ifndef FOO_CORE_HPP_
2+
#define FOO_CORE_HPP_
3+
4+
#include "rclcpp/rclcpp.hpp"
5+
6+
class FooCore {
7+
public:
8+
/*
9+
* Foo core constructor.
10+
*/
11+
FooCore();
12+
13+
private:
14+
15+
};
16+
17+
#endif
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
class FooCore():
3+
def __init__(self):
4+
pass
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#include "foo_node.hpp"
2+
3+
FooNode::FooNode() {
4+
5+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#ifndef FOO_NODE_HPP_
2+
#define FOO_NODE_HPP_
3+
4+
#include "rclcpp/rclcpp.hpp"
5+
6+
#include "foo_core.hpp"
7+
8+
class FooNode : public rclcpp::Node {
9+
public:
10+
/*
11+
* Foo node constructor.
12+
*/
13+
FooNode();
14+
15+
private:
16+
17+
};
18+
19+
#endif
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import rclpy
2+
from rclpy.node import Node
3+
4+
from foo.foo_core import FooCore
5+
6+
class Foo(Node):
7+
def __init__(self):
8+
super().__init__('python_foo')
9+
10+
# Declare and get the parameters
11+
self.declare_parameter('version', 1)
12+
13+
self.__foo = FooCore()
14+
15+
def main(args=None):
16+
rclpy.init(args=args)
17+
18+
python_foo = Foo()
19+
20+
rclpy.spin(python_foo)
21+
22+
# Destroy the node explicitly
23+
# (optional - otherwise it will be done automatically
24+
# when the garbage collector destroys the node object)
25+
python_foo.destroy_node()
26+
rclpy.shutdown()
27+
28+
if __name__ == '__main__':
29+
main()
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
#include "rclcpp/rclcpp.hpp"
2+
#include "gtest/gtest.h"
3+
// Google testing documentation at https://google.github.io/googletest/primer.html
4+
5+
#include "foo_core.hpp"
6+
7+
// The fixture for testing class Foo.
8+
class FooTest : public testing::Test {
9+
protected:
10+
// You can remove any or all of the following functions if their bodies would
11+
// be empty.
12+
13+
FooTest() {
14+
// You can do set-up work for each test here.
15+
}
16+
17+
~FooTest() override {
18+
// You can do clean-up work that doesn't throw exceptions here.
19+
}
20+
21+
// If the constructor and destructor are not enough for setting up
22+
// and cleaning up each test, you can define the following methods:
23+
24+
void SetUp() override {
25+
// Code here will be called immediately after the constructor (right
26+
// before each test).
27+
}
28+
29+
void TearDown() override {
30+
// Code here will be called immediately after each test (right
31+
// before the destructor).
32+
}
33+
34+
// Class members declared here can be used by all tests in the test suite
35+
// for Foo.
36+
};
37+
38+
// Tests that the Foo::Bar() method does Abc.
39+
TEST_F(FooTest, MethodBarDoesAbc) {
40+
const std::string input_filepath = "this/package/testdata/myinputfile.dat";
41+
const std::string output_filepath = "this/package/testdata/myoutputfile.dat";
42+
Foo f;
43+
EXPECT_EQ(f.Bar(input_filepath, output_filepath), 0);
44+
}
45+
46+
// Tests that Foo does Xyz.
47+
TEST_F(FooTest, DoesXyz) {
48+
// Exercises the Xyz feature of Foo.
49+
}
50+
51+
int main(int argc, char **argv) {
52+
testing::InitGoogleTest(&argc, argv);
53+
return RUN_ALL_TESTS();
54+
}

0 commit comments

Comments
 (0)