Skip to content

Commit 33fb35a

Browse files
authored
Add urdf tooling! (#7)
1 parent dd537cc commit 33fb35a

76 files changed

Lines changed: 1508 additions & 9 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cruft.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"template": "https://github.com/UrbanMachine/create-ros-app.git",
3-
"commit": "7697b9d1edf4d2b27d2bc9d3095fa893a8d92497",
3+
"commit": "6a85fd934e7c6297d5fd717c545fad443cc4dfcf",
44
"checkout": null,
55
"context": {
66
"cookiecutter": {

.github/workflows/test.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ jobs:
8989
defaults:
9090
run:
9191
shell: bash
92-
timeout-minutes: 40
92+
timeout-minutes: 15
9393
container:
9494
image: ${{needs.build-image.outputs.tagged_image}}
9595
credentials:

docker/Dockerfile

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ RUN --mount=type=cache,target="${APT_CACHE}" \
4141
# ROS2
4242
ros-${ROS2_DISTRO}-ros-base \
4343
ros-${ROS2_DISTRO}-rosbridge-suite \
44+
ros-${ROS2_DISTRO}-rmw-cyclonedds-cpp \
4445
# Build tools
4546
build-essential \
4647
git \
@@ -101,13 +102,8 @@ COPY pkgs/node_helpers/pyproject.toml pkgs/node_helpers/pyproject.toml
101102
########## Add Git ROS2 Packages
102103
########## NOTE TO TEMPLATE USERS: If you need to depend on a package that is not in the ROS2 distro, you can add it here
103104
WORKDIR /ros-git-deps/
104-
RUN --mount=type=cache,target="${PIP_CACHE}" \
105-
--mount=type=cache,target="${APT_CACHE}" \
106-
install-ros-package-from-git \
107-
https://github.com/UrbanMachine/node_helpers.git main pkgs && \
108105
##################### Add your packages here!
109106
########### install-ros-package-from-git {URL} {BRANCH} {PKGS PATH IN REPO}
110-
echo "Done installing ROS2 packages from git"
111107
######################################################################
112108
113109
# Install Poetry dependencies for each package in this repo

docs/launching.rst

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Launching
22
=========
33

4-
The `node_helpers.launching` module provides utility functions and classes to streamline the management of ROS launch files.
4+
The `node_helpers.launching` module provides utility functions and classes to streamline the management of ROS launch files and URDF configurations. This module is particularly useful for handling dynamic node swapping and ensuring file existence for launch operations.
55

66
Core Features
77
-------------
@@ -45,6 +45,9 @@ Core Features
4545
4646
config_file = launching.required_file("/path/to/config.yaml")
4747
48+
3. **URDF Manipulation**:
49+
For information on urdf launching, look into at the ``urdfs`` docs.
50+
4851
Error Handling and Validation
4952
-----------------------------
5053

docs/urdfs.rst

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
URDF Utilities
2+
==============
3+
4+
There are several URDF tools in `node_helpers` for launching, validating, and testing URDF-based systems, providing a standardized approach to handling URDFs in robotics applications.
5+
6+
Overview
7+
--------
8+
9+
The module includes the following components:
10+
1. **node_helpers.launching.URDFModuleNodeFactory**: Streamlines creation of `joint_state_publisher` and `robot_state_publisher` for URDFs in launch files.
11+
2. **node_helpers.urdfs.URDFConstant**: Provides consistent access to URDF frames and joints, with validation tools. This is key for accessing 'tf' frames and 'joints' in a standardized way, without having random string constants sprinkled around your codebase.
12+
3. **node_helpers.testing.URDFModuleFixture**: Facilitates launching URDF modules for integration tests.
13+
14+
URDFConstant
15+
------------
16+
17+
The `URDFConstants` class provides a structured way to reference and validate URDF elements, such as joints and frames. It ensures URDF correctness and avoids duplicate names or missing elements.
18+
19+
**Features**:
20+
21+
- Load multiple URDF's but refer to them as a single module in code.
22+
- Prepend namespaces to avoid conflicts.
23+
- Validate that joints and frames exist in the URDF.
24+
- Dynamically adjust URDFs with namespaces.
25+
26+
**Example**:
27+
28+
Below, we create the concept of a "BigBird" robot, which consists of two URDFs.
29+
We then, at the bottom, create a `BigBirdURDF` object that encapsulates the URDFs and provides access to the joints and frames.
30+
31+
The BigBirdJoint and BigBirdFrames classes define the joints and frames in the URDFs,
32+
and refer to real URDF elements by their names, prepended with `bird_gantry` or `bird_base`
33+
to point back to what URDF file they came from. The `urdf_paths` parameter in the `URDFConstants` constructor
34+
specifies what URDF the prepended names refer to.
35+
36+
.. code-block:: python
37+
38+
from typing import NamedTuple
39+
40+
from urdf_data.urdf_constants import URDFConstants
41+
42+
43+
class BigBirdJoints(NamedTuple):
44+
X: str = "bird_gantry.xaxis"
45+
Y: str = "bird_gantry.yaxis"
46+
Z: str = "bird_gantry.zaxis"
47+
PAN: str = "bird_gantry.waxis"
48+
49+
50+
class BigBirdFrames(NamedTuple):
51+
BASE_LINK: str = "bird_base.gantry_base_link"
52+
53+
X_AXIS_ORIGIN: str = "bird_gantry.xaxis_parent_datum"
54+
X_AXIS_CURRENT: str = "bird_gantry.gantry_xlink"
55+
56+
Y_AXIS_ORIGIN: str = "bird_gantry.yaxis_parent_datum"
57+
Y_AXIS_CURRENT: str = "bird_gantry.gantry_ylink"
58+
59+
Z_AXIS_ORIGIN: str = "bird_gantry.zaxis_parent_datum"
60+
Z_AXIS_CURRENT: str = "bird_gantry.gantry_zlink"
61+
62+
PAN_ORIGIN: str = "bird_gantry.waxis_parent_datum"
63+
PAN_CURRENT: str = "bird_gantry.gantry_wlink"
64+
65+
66+
TOOL_TIP: str = "bird.grasp_point"
67+
68+
69+
BigBirdURDF = URDFConstants[BigBirdJoints, BigBirdFrames](
70+
registration_name="bird_robot",
71+
urdf_paths=[
72+
("bird_base", "path/to/bird_base/robot.urdf"),
73+
("bird_gantry", "path/to/bird_gantry/robot.urdf"),
74+
joints=BigBirdJoints(),
75+
frames=BigBirdFrames(),
76+
)
77+
78+
Note that an example URDF constant can be found in ``pkgs/node_helpers_test/integration/urdfs/example_urdf_constants.py``
79+
80+
URDFModule
81+
----------
82+
83+
The `URDFModuleNodeFactory` simplifies launching URDF nodes by generating `robot_state_publisher` and `joint_state_publisher` nodes for each URDF file. It applies namespaces to avoid collisions and ensures URDFs are properly loaded and validated.
84+
85+
In the below example, a ``joint_state_publisher`` will be created under the ``/big_bird_left/`` namespace,
86+
and multiple ``robot_state_publishers`` will be created for each URDF file in the `BigBirdURDF` constant.
87+
For example, one will live under ``/big_bird_left/urdf_0/`` and the other under ``/big_bird_left/urdf_1/``.
88+
89+
They will all publish to the same ``joint_state_publisher`` under the ``/big_bird_left/`` namespace.
90+
91+
**Example**:
92+
93+
.. code-block:: python
94+
95+
from node_helpers.urdfs.urdf_module_launching import URDFModuleNodeFactory
96+
97+
parameters = URDFModuleNodeFactory.Parameters(
98+
namespace: "big_bird_left",
99+
urdf_constant_name: "BigBirdURDF",
100+
apply_namespace_to_urdf: True,
101+
)
102+
factory = URDFModuleNodeFactory(parameters)
103+
nodes = factory.create_nodes() # these nodes can be added to a launch description
104+
105+
106+
URDFModuleFixture
107+
------------------
108+
109+
The ``URDFModuleFixture`` class is a pytest fixture utility for setting up URDF-based tests. It
110+
will launch the URDF module (and all it's `robot_state_publisher`s and `joint_state_publisher`,
111+
and ensure that all TF frames are published correctly before yielding the fixture.
112+
113+
**Example**:
114+
115+
.. code-block:: python
116+
117+
from node_helpers.urdfs.urdf_module_fixture import URDFModuleFixture
118+
119+
@pytest.fixture()
120+
def big_bird_urdf_module() -> Generator[URDFModuleFixture, None, None]:
121+
yield from URDFModuleFixture.set_up(
122+
URDFModuleNodeFactory.Parameters(
123+
namespace="big_bird_top", urdf_constant_name=BigBirdURDF.registration_name
124+
)
125+
)
126+
127+
128+
A full example of how to integration test URDFs can be found under ``pkgs/node_helpers/node_helpers_test/integration/urdfs/test_forklift.py``
129+
130+
Note that ``node_helpers`` provides a helpful test URDF in ``pkgs/node_helpers/sample_urdfs/forklift/robot.urdf``

pkgs/node_helpers/node_helpers/launching/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
SwappableNode,
66
apply_node_swaps,
77
)
8+
from .urdf_module_launching import URDFModuleNodeFactory
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
from functools import reduce
2+
from operator import iconcat
3+
from typing import Any
4+
5+
from launch_ros.actions import Node
6+
from pydantic import BaseModel
7+
8+
from node_helpers.urdfs.urdf_constants import URDFConstants
9+
10+
11+
class URDFModuleNodeFactory:
12+
"""A helper object for creating nodes for a urdf module.
13+
14+
This class takes a URDFConstant and a namespace, and then for each child URDF will:
15+
1) Spin up a robot state publisher under the namespace '/{namespace}/urdf_{idx}'
16+
2) Spin up a joint state publisher under the above namespace.
17+
18+
If there are 3 URDFs in the URDFConstant, there will be 6 nodes total launched.
19+
Potential future optimizations include only spinning up `joint_state_publishers` if
20+
there happen to be any joints in the URDF being spun up.
21+
"""
22+
23+
class Parameters(BaseModel):
24+
namespace: str
25+
"""The namespace under which under which joint state publishers and robot state
26+
publishers will live, a la /{namespace}/urdf_# """
27+
28+
urdf_constant_name: str
29+
"""The chosen URDFConstant.registration_name to spin up. In configuration, you
30+
can reference these as strings, using the name attribute to load a specific
31+
instance of a URDFConstant."""
32+
33+
apply_namespace_to_urdf: bool = True
34+
"""If True, the node namespace will be prepended to the URDF frames. This is
35+
the behaviour used by hardware modules. Set this to False if your URDF is not
36+
part of a hardware module."""
37+
38+
def __init__(self, parameters: Parameters):
39+
self._params = parameters
40+
41+
# Create the URDFConstant, with a namespace optionally prepended
42+
base_urdf_constants = URDFConstants[Any, Any].get_registered_instance(
43+
self._params.urdf_constant_name
44+
)
45+
self.urdf_constants = (
46+
base_urdf_constants.with_namespace(self._params.namespace)
47+
if self._params.apply_namespace_to_urdf
48+
else base_urdf_constants
49+
)
50+
51+
def create_nodes(self) -> list[Node]:
52+
"""Create the nodes required to load and visualize each specified urdf path"""
53+
urdf_strs = self.urdf_constants.load_urdfs()
54+
55+
urdf_nodes = [
56+
[
57+
self.create_robot_state_publisher(
58+
namespace=self._params.namespace,
59+
urdf_index=urdf_index,
60+
urdf_str=urdf_str,
61+
),
62+
self.create_joint_state_publisher(
63+
namespace=self._params.namespace,
64+
urdf_index=urdf_index,
65+
),
66+
]
67+
for urdf_index, urdf_str in enumerate(urdf_strs)
68+
]
69+
return reduce(iconcat, urdf_nodes, [])
70+
71+
@staticmethod
72+
def create_joint_state_publisher(namespace: str, urdf_index: int) -> Node:
73+
return Node(
74+
package="joint_state_publisher",
75+
namespace=URDFModuleNodeFactory.urdf_namespace(namespace, urdf_index),
76+
executable="joint_state_publisher",
77+
parameters=[
78+
{
79+
"source_list": [
80+
f"/{namespace}/desired_joint_states",
81+
]
82+
}
83+
],
84+
)
85+
86+
@staticmethod
87+
def create_robot_state_publisher(
88+
namespace: str,
89+
urdf_index: int,
90+
urdf_str: str,
91+
) -> Node:
92+
"""Create a robot state publisher using the hardware module standards
93+
:param namespace: The namespace under which to create the urdf namespace
94+
:param urdf_index: The index of this urdf within the parent namespace
95+
:param urdf_str: The urdf as a string to pass to the robot_state_publisher
96+
:return: The robot state publisher node.
97+
"""
98+
99+
return Node(
100+
package="robot_state_publisher",
101+
namespace=URDFModuleNodeFactory.urdf_namespace(namespace, urdf_index),
102+
executable="robot_state_publisher",
103+
parameters=[
104+
{"robot_description": urdf_str},
105+
],
106+
)
107+
108+
@property
109+
def urdf_namespaces(self) -> list[str]:
110+
"""Returns the namespaces under which URDFs are stored, for rviz remapping."""
111+
return [
112+
self.urdf_namespace(self._params.namespace, urdf_id)
113+
for urdf_id in range(len(self.urdf_constants))
114+
]
115+
116+
@staticmethod
117+
def urdf_namespace(namespace: str, urdf_index: int) -> str:
118+
"""A helper for creating the namespace for a given urdf in a module
119+
120+
:param namespace: The parent namespace that will own one or more URDFs
121+
:param urdf_index: The index of this particular urdf
122+
:return: The formatted namespace string
123+
"""
124+
return f"{namespace}/urdf_{urdf_index}"

pkgs/node_helpers/node_helpers/testing/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,15 @@
2020
from .resources import MessageResource, NumpyResource, resource_path
2121
from .threads import ContextThread, DynamicContextThread, get_unclosed_threads
2222
from .transforms import set_up_static_transforms
23+
from .urdf_frame_validation import (
24+
validate_coincident_transforms,
25+
validate_expected_rotation,
26+
)
27+
from .urdf_module_fixture import (
28+
TFClient,
29+
URDFFixtureSetupFailed,
30+
URDFModuleFixture,
31+
)
2332

2433
faulthandler.enable()
2534

@@ -42,4 +51,9 @@
4251
"set_up_node",
4352
"rclpy_context",
4453
"run_and_cancel_task",
54+
"URDFModuleFixture",
55+
"TFClient",
56+
"URDFFixtureSetupFailed",
57+
"validate_coincident_transforms",
58+
"validate_expected_rotation",
4559
]

0 commit comments

Comments
 (0)