Skip to content

Commit 16802e9

Browse files
authored
Feat/landmark support (#30)
* landmark array to pose vector * landmark transform * workflow msgs dependency * split declarations
1 parent 9f26976 commit 16802e9

11 files changed

Lines changed: 122 additions & 0 deletions

File tree

.github/workflows/code-coverage.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ jobs:
1111
call_reusable_workflow:
1212
uses: vortexntnu/vortex-ci/.github/workflows/reusable-code-coverage.yml@main
1313
with:
14+
vcs-repo-file-url: './dependencies.repos'
1415
before_install_target_dependencies: 'scripts/ci_install_dependencies.sh'
1516
secrets:
1617
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

.github/workflows/industrial-ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ jobs:
1010
call_reusable_workflow:
1111
uses: vortexntnu/vortex-ci/.github/workflows/reusable-industrial-ci.yml@main
1212
with:
13+
upstream_workspace: './dependencies.repos'
1314
ros_repo: '["main", "testing"]'
1415
before_install_target_dependencies: 'scripts/ci_install_dependencies.sh'

dependencies.repos

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
repositories:
2+
vortex-msgs:
3+
type: git
4+
url: https://github.com/vortexntnu/vortex-msgs.git

vortex_utils_ros/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ find_package(rclcpp REQUIRED)
1010
find_package(ament_cmake_python REQUIRED)
1111
find_package(Eigen3 REQUIRED)
1212
find_package(geometry_msgs REQUIRED)
13+
find_package(vortex_msgs REQUIRED)
1314

1415
add_library(vortex_utils_ros INTERFACE)
1516

@@ -23,6 +24,7 @@ ament_target_dependencies(vortex_utils_ros INTERFACE
2324
rclcpp
2425
Eigen3
2526
geometry_msgs
27+
vortex_msgs
2628
)
2729

2830
install(

vortex_utils_ros/cpp_test/test_ros_conversions.cpp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <geometry_msgs/msg/pose_array.hpp>
88
#include <geometry_msgs/msg/pose_stamped.hpp>
99
#include <geometry_msgs/msg/pose_with_covariance_stamped.hpp>
10+
#include <vortex_msgs/msg/landmark_array.hpp>
1011

1112
#include <vortex/utils/math.hpp>
1213
#include <vortex/utils/types.hpp>
@@ -214,3 +215,17 @@ TEST(ros_to_pose_vec, pose_array) {
214215
EXPECT_NEAR(v[0].x, 1.0, 1e-6);
215216
EXPECT_NEAR(v[1].x, 2.0, 1e-6);
216217
}
218+
219+
TEST(ros_to_pose_vec, landmark_array) {
220+
vortex_msgs::msg::LandmarkArray arr;
221+
arr.landmarks.resize(2);
222+
223+
arr.landmarks[0].pose.pose.position.x = 1;
224+
arr.landmarks[1].pose.pose.position.x = 2;
225+
226+
auto v = vortex::utils::ros_conversions::ros_to_pose_vec(arr);
227+
228+
ASSERT_EQ(v.size(), 2);
229+
EXPECT_NEAR(v[0].x, 1.0, 1e-6);
230+
EXPECT_NEAR(v[1].x, 2.0, 1e-6);
231+
}

vortex_utils_ros/include/vortex/utils/ros/ros_conversions.hpp

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <geometry_msgs/msg/pose_stamped.hpp>
1414
#include <geometry_msgs/msg/pose_with_covariance.hpp>
1515
#include <geometry_msgs/msg/pose_with_covariance_stamped.hpp>
16+
#include <vortex_msgs/msg/landmark_array.hpp>
1617

1718
#include <vortex/utils/concepts.hpp>
1819
#include <vortex/utils/math.hpp>
@@ -159,6 +160,22 @@ inline std::vector<vortex::utils::types::Pose> ros_to_pose_vec(
159160
return poses;
160161
}
161162

163+
/**
164+
* @brief Converts a ROS vortex_msgs::msg::LandmarkArray to an internal Pose
165+
* vector type.
166+
* @param pose vortex_msgs::msg::LandmarkArray
167+
* @return std::vector<vortex::utils::types::Pose> Internal pose representation
168+
*/
169+
inline std::vector<vortex::utils::types::Pose> ros_to_pose_vec(
170+
const vortex_msgs::msg::LandmarkArray& msg) {
171+
std::vector<vortex::utils::types::Pose> poses;
172+
poses.reserve(msg.landmarks.size());
173+
for (const auto& landmark : msg.landmarks) {
174+
poses.push_back(ros_pose_to_pose(landmark.pose.pose));
175+
}
176+
return poses;
177+
}
178+
162179
} // namespace vortex::utils::ros_conversions
163180

164181
#endif // VORTEX_UTILS__ROS_CONVERSIONS_HPP_

vortex_utils_ros/package.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<depend>python3-numpy</depend>
1717
<depend>python3-scipy</depend>
1818
<depend>geometry_msgs</depend>
19+
<depend>vortex_msgs</depend>
1920

2021

2122
<test_depend>ament_cmake_pytest</test_depend>

vortex_utils_ros_tf/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ find_package(geometry_msgs REQUIRED)
1111
find_package(tf2 REQUIRED)
1212
find_package(tf2_ros REQUIRED)
1313
find_package(tf2_geometry_msgs REQUIRED)
14+
find_package(vortex_msgs REQUIRED)
1415

1516
add_library(vortex_utils_ros_tf INTERFACE)
1617

@@ -25,6 +26,7 @@ ament_target_dependencies(vortex_utils_ros_tf INTERFACE
2526
tf2_ros
2627
tf2_geometry_msgs
2728
geometry_msgs
29+
vortex_msgs
2830
)
2931

3032
install(

vortex_utils_ros_tf/cpp_test/test_ros_transforms.cpp

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include <geometry_msgs/msg/pose_stamped.hpp>
77
#include <geometry_msgs/msg/pose_with_covariance_stamped.hpp>
88
#include <geometry_msgs/msg/transform_stamped.hpp>
9+
#include <vortex_msgs/msg/landmark_array.hpp>
910

1011
#include <tf2_ros/buffer.h>
1112
#include <tf2_ros/static_transform_broadcaster.h>
@@ -142,6 +143,39 @@ TEST_F(RosTransformsTest, pose_array_failure) {
142143
tf2::TransformException);
143144
}
144145

146+
TEST_F(RosTransformsTest, landmark_array_failure) {
147+
vortex_msgs::msg::LandmarkArray in, out;
148+
in.header.frame_id = "unknown_frame";
149+
in.header.stamp = node_->get_clock()->now();
150+
in.landmarks.resize(1);
151+
152+
EXPECT_THROW(
153+
{
154+
vortex::utils::ros_transforms::transform_pose(*buffer_, in,
155+
"target", out);
156+
},
157+
tf2::TransformException);
158+
}
159+
160+
TEST_F(RosTransformsTest, landmark_array_success) {
161+
vortex_msgs::msg::LandmarkArray in, out;
162+
in.header.frame_id = "source";
163+
in.header.stamp = node_->get_clock()->now();
164+
165+
in.landmarks.resize(2);
166+
in.landmarks[0].pose.pose.position.x = 0.0;
167+
in.landmarks[1].pose.pose.position.x = 1.0;
168+
169+
EXPECT_NO_THROW({
170+
vortex::utils::ros_transforms::transform_pose(*buffer_, in, "target",
171+
out);
172+
});
173+
174+
ASSERT_EQ(out.landmarks.size(), 2);
175+
EXPECT_NEAR(out.landmarks[0].pose.pose.position.x, -1.0, 1e-6);
176+
EXPECT_NEAR(out.landmarks[1].pose.pose.position.x, 0.0, 1e-6);
177+
}
178+
145179
int main(int argc, char** argv) {
146180
rclcpp::init(argc, argv);
147181
::testing::InitGoogleTest(&argc, argv);

vortex_utils_ros_tf/include/vortex/utils/ros/ros_transforms.hpp

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
#include <geometry_msgs/msg/pose_array.hpp>
55
#include <geometry_msgs/msg/pose_stamped.hpp>
66
#include <geometry_msgs/msg/pose_with_covariance_stamped.hpp>
7+
#include <vortex_msgs/msg/landmark_array.hpp>
78

89
#include <tf2/exceptions.h>
910
#include <tf2/time.h>
@@ -103,6 +104,49 @@ inline void transform_pose(tf2_ros::Buffer& tf_buffer,
103104
}
104105
}
105106

107+
/**
108+
* @brief Transform all poses in a LandmarkArray into a target frame using TF.
109+
*
110+
* Each pose in the input LandmarkArray is individually transformed using the
111+
* array header (frame ID and timestamp). The output LandmarkArray contains
112+
* only successfully transformed poses and is expressed in the target frame.
113+
*
114+
* TF does not natively support PoseArray, so each pose is internally
115+
* wrapped as a PoseStamped for transformation.
116+
*
117+
* @param tf_buffer TF buffer used for transform lookup
118+
* @param in Input PoseArray message
119+
* @param target_frame Target frame ID
120+
* @param out Output PoseArray message in the target frame
121+
* @param timeout Maximum duration to wait for each pose transform
122+
* @note This function throws tf2::TransformException on failure.
123+
* Callers are expected to handle errors via try/catch.
124+
*/
125+
inline void transform_pose(tf2_ros::Buffer& tf_buffer,
126+
const vortex_msgs::msg::LandmarkArray& in,
127+
const std::string& target_frame,
128+
vortex_msgs::msg::LandmarkArray& out,
129+
tf2::Duration timeout = tf2::durationFromSec(0.0)) {
130+
out.header.stamp = in.header.stamp;
131+
out.header.frame_id = target_frame;
132+
out.landmarks.clear();
133+
out.landmarks.reserve(in.landmarks.size());
134+
135+
for (const auto& lm : in.landmarks) {
136+
vortex_msgs::msg::Landmark lm_out = lm;
137+
138+
geometry_msgs::msg::PoseWithCovarianceStamped in_ps;
139+
geometry_msgs::msg::PoseWithCovarianceStamped out_ps;
140+
in_ps.header = in.header;
141+
in_ps.pose = lm.pose;
142+
143+
tf_buffer.transform(in_ps, out_ps, target_frame, timeout);
144+
145+
lm_out.pose = out_ps.pose;
146+
out.landmarks.push_back(lm_out);
147+
}
148+
}
149+
106150
} // namespace vortex::utils::ros_transforms
107151

108152
#endif // VORTEX_UTILS__ROS_TRANSFORMS_HPP_

0 commit comments

Comments
 (0)