Skip to content

Commit 7320dd0

Browse files
committed
Add tests for OdometryPublisherSystem and fix velocity calculation bug
Swap X and Y linear velocities when calculating odometry velocities relative to robotBaseFrame. Signed-off-by: Maganty Rushyendra <[email protected]>
1 parent 72f774b commit 7320dd0

File tree

6 files changed

+787
-8
lines changed

6 files changed

+787
-8
lines changed

src/systems/odometry_publisher/OdometryPublisher.cc

+7-6
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ void OdometryPublisher::Configure(const Entity &_entity,
114114
return;
115115
}
116116

117-
this->dataPtr->odomFrame = "odom";
117+
this->dataPtr->odomFrame = this->dataPtr->model.Name(_ecm) + "/" + "odom";
118118
if (!_sdf->HasElement("odom_frame"))
119119
{
120120
ignwarn << "OdometryPublisher system plugin missing <odom_frame>, "
@@ -125,7 +125,8 @@ void OdometryPublisher::Configure(const Entity &_entity,
125125
this->dataPtr->odomFrame = _sdf->Get<std::string>("odom_frame");
126126
}
127127

128-
this->dataPtr->robotBaseFrame = "base_footprint";
128+
this->dataPtr->robotBaseFrame = this->dataPtr->model.Name(_ecm)
129+
+ "/" + "base_footprint";
129130
if (!_sdf->HasElement("robot_base_frame"))
130131
{
131132
ignwarn << "OdometryPublisher system plugin missing <robot_base_frame>, "
@@ -238,9 +239,9 @@ void OdometryPublisherPrivate::UpdateOdometry(
238239

239240
// Get velocities in robotBaseFrame and add to message.
240241
double linearVelocityX = (cosf(currentYaw) * linearDisplacementX
241-
+ sinf(currentYaw) * linearDisplacementX) / dt.count();
242+
+ sinf(currentYaw) * linearDisplacementY) / dt.count();
242243
double linearVelocityY = (cosf(currentYaw) * linearDisplacementY
243-
- sinf(currentYaw) * linearDisplacementY) / dt.count();
244+
- sinf(currentYaw) * linearDisplacementX) / dt.count();
244245
this->linearMean.first.Push(linearVelocityX);
245246
this->linearMean.second.Push(linearVelocityY);
246247
this->angularMean.Push(angularDiff / dt.count());
@@ -255,10 +256,10 @@ void OdometryPublisherPrivate::UpdateOdometry(
255256
// Set the frame ids.
256257
auto frame = msg.mutable_header()->add_data();
257258
frame->set_key("frame_id");
258-
frame->add_value(this->model.Name(_ecm) + "/" + odomFrame);
259+
frame->add_value(odomFrame);
259260
auto childFrame = msg.mutable_header()->add_data();
260261
childFrame->set_key("child_frame_id");
261-
childFrame->add_value(this->model.Name(_ecm) + "/" + robotBaseFrame);
262+
childFrame->add_value(robotBaseFrame);
262263

263264
this->lastUpdatePose = pose;
264265
this->lastUpdateTime = std::chrono::steady_clock::time_point(_info.simTime);

src/systems/odometry_publisher/OdometryPublisher.hh

+2-2
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ namespace systems
4040
///
4141
/// `<odom_frame>`: Name of the world-fixed coordinate frame for the
4242
// odometry message. This element is optional, and the default value
43-
/// is `odom`.
43+
/// is `{name_of_model}/odom`.
4444
///
4545
/// `<robot_base_frame>`: Name of the coordinate frame rigidly attached
4646
/// to the mobile robot base. This element is optional, and the default
47-
/// value is `robot_base_frame`.
47+
/// value is `{name_of_model}/base_footprint`.
4848
///
4949
/// `<odom_publish_frequency>`: Odometry publication frequency. This
5050
/// element is optional, and the default value is 50Hz.

test/integration/CMakeLists.txt

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ set(tests
3131
model.cc
3232
multicopter.cc
3333
network_handshake.cc
34+
odometry_publisher.cc
3435
performer_detector.cc
3536
physics_system.cc
3637
play_pause.cc
+313
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,313 @@
1+
/*
2+
* Copyright (C) 2021 Open Source Robotics Foundation
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
18+
#include <gtest/gtest.h>
19+
#include <ignition/common/Console.hh>
20+
#include <ignition/math/Pose3.hh>
21+
#include <ignition/transport/Node.hh>
22+
23+
#include "ignition/gazebo/components/AngularVelocity.hh"
24+
#include "ignition/gazebo/components/AngularVelocityCmd.hh"
25+
#include "ignition/gazebo/components/LinearVelocity.hh"
26+
#include "ignition/gazebo/components/LinearVelocityCmd.hh"
27+
#include "ignition/gazebo/components/Name.hh"
28+
#include "ignition/gazebo/components/Model.hh"
29+
#include "ignition/gazebo/components/Pose.hh"
30+
#include "ignition/gazebo/Server.hh"
31+
#include "ignition/gazebo/SystemLoader.hh"
32+
#include "ignition/gazebo/test_config.hh"
33+
34+
#include "../helpers/Relay.hh"
35+
36+
#define tol 0.005
37+
38+
using namespace ignition;
39+
using namespace gazebo;
40+
using namespace std::chrono_literals;
41+
42+
/// \brief Test OdometryPublisher system
43+
class OdometryPublisherTest : public ::testing::TestWithParam<int>
44+
{
45+
// Documentation inherited
46+
protected: void SetUp() override
47+
{
48+
common::Console::SetVerbosity(4);
49+
setenv("IGN_GAZEBO_SYSTEM_PLUGIN_PATH",
50+
(std::string(PROJECT_BINARY_PATH) + "/lib").c_str(), 1);
51+
}
52+
53+
/// \param[in] _sdfFile SDF file to load.
54+
/// \param[in] _odomTopic Odometry topic.
55+
protected: void TestMovement(const std::string &_sdfFile,
56+
const std::string &_odomTopic)
57+
{
58+
// Start server
59+
ServerConfig serverConfig;
60+
serverConfig.SetSdfFile(_sdfFile);
61+
62+
Server server(serverConfig);
63+
EXPECT_FALSE(server.Running());
64+
EXPECT_FALSE(*server.Running(0));
65+
66+
// Create a system that records the vehicle poses and velocities
67+
test::Relay testSystem;
68+
69+
std::vector<math::Pose3d> poses;
70+
testSystem.OnPostUpdate([&poses](const gazebo::UpdateInfo &,
71+
const gazebo::EntityComponentManager &_ecm)
72+
{
73+
auto id = _ecm.EntityByComponents(
74+
components::Model(),
75+
components::Name("vehicle"));
76+
EXPECT_NE(kNullEntity, id);
77+
78+
auto poseComp = _ecm.Component<components::Pose>(id);
79+
ASSERT_NE(nullptr, poseComp);
80+
poses.push_back(poseComp->Data());
81+
});
82+
server.AddSystem(testSystem.systemPtr);
83+
84+
// Run server while model is stationary
85+
server.Run(true, 1000, false);
86+
87+
EXPECT_EQ(1000u, poses.size());
88+
89+
for (const auto &pose : poses)
90+
{
91+
EXPECT_EQ(poses[0], pose);
92+
}
93+
94+
// 50 Hz is the default publishing freq
95+
double period{1.0 / 50.0};
96+
double lastMsgTime{1.0};
97+
std::vector<math::Pose3d> odomPoses;
98+
std::vector<math::Vector3d> odomLinVels;
99+
std::vector<math::Vector3d> odomAngVels;
100+
// Create function to store data from odometry messages
101+
std::function<void(const msgs::Odometry &)> odomCb =
102+
[&](const msgs::Odometry &_msg)
103+
{
104+
ASSERT_TRUE(_msg.has_header());
105+
ASSERT_TRUE(_msg.header().has_stamp());
106+
107+
double msgTime =
108+
static_cast<double>(_msg.header().stamp().sec()) +
109+
static_cast<double>(_msg.header().stamp().nsec()) * 1e-9;
110+
111+
EXPECT_DOUBLE_EQ(msgTime, lastMsgTime + period);
112+
lastMsgTime = msgTime;
113+
114+
odomPoses.push_back(msgs::Convert(_msg.pose()));
115+
odomLinVels.push_back(msgs::Convert(_msg.twist().linear()));
116+
odomAngVels.push_back(msgs::Convert(_msg.twist().angular()));
117+
};
118+
transport::Node node;
119+
node.Subscribe(_odomTopic, odomCb);
120+
121+
test::Relay velocityRamp;
122+
math::Vector3d linVelCmd(1, 0.5, 0.0);
123+
math::Vector3d angVelCmd(0.0, 0.0, 0.2);
124+
velocityRamp.OnPreUpdate(
125+
[&](const gazebo::UpdateInfo &/*_info*/,
126+
gazebo::EntityComponentManager &_ecm)
127+
{
128+
auto en = _ecm.EntityByComponents(
129+
components::Model(),
130+
components::Name("vehicle"));
131+
EXPECT_NE(kNullEntity, en);
132+
133+
// Set the linear velocity of the model
134+
auto linVelCmdComp =
135+
_ecm.Component<components::LinearVelocityCmd>(en);
136+
if (!linVelCmdComp)
137+
{
138+
_ecm.CreateComponent(en,
139+
components::LinearVelocityCmd(linVelCmd));
140+
}
141+
else
142+
{
143+
linVelCmdComp->Data() = linVelCmd;
144+
}
145+
146+
// Set the angular velocity of the model
147+
auto angVelCmdComp =
148+
_ecm.Component<components::AngularVelocityCmd>(en);
149+
if (!angVelCmdComp)
150+
{
151+
_ecm.CreateComponent(en,
152+
components::AngularVelocityCmd(angVelCmd));
153+
}
154+
else
155+
{
156+
angVelCmdComp->Data() = angVelCmd;
157+
}
158+
});
159+
160+
server.AddSystem(velocityRamp.systemPtr);
161+
162+
// Run server while the model moves with the velocities set earlier
163+
server.Run(true, 3000, false);
164+
165+
// Poses for 4s
166+
ASSERT_EQ(4000u, poses.size());
167+
168+
int sleep = 0;
169+
int maxSleep = 30;
170+
for (; odomPoses.size() < 150 && sleep < maxSleep; ++sleep)
171+
{
172+
std::this_thread::sleep_for(std::chrono::milliseconds(100));
173+
}
174+
ASSERT_NE(maxSleep, sleep);
175+
176+
// Odom for 3s
177+
ASSERT_FALSE(odomPoses.empty());
178+
EXPECT_EQ(150u, odomPoses.size());
179+
EXPECT_EQ(150u, odomLinVels.size());
180+
EXPECT_EQ(150u, odomAngVels.size());
181+
182+
// Check accuracy of poses published in the odometry message
183+
auto finalModelFramePose = odomPoses.back();
184+
EXPECT_NEAR(poses[1020].Pos().X(), odomPoses[0].Pos().X(), 1e-2);
185+
EXPECT_NEAR(poses[1020].Pos().Y(), odomPoses[0].Pos().Y(), 1e-2);
186+
EXPECT_NEAR(poses[1020].Pos().Z(), odomPoses[0].Pos().Z(), 1e-2);
187+
EXPECT_NEAR(poses[1020].Rot().X(), odomPoses[0].Rot().X(), 1e-2);
188+
EXPECT_NEAR(poses[1020].Rot().Y(), odomPoses[0].Rot().Y(), 1e-2);
189+
EXPECT_NEAR(poses[1020].Rot().Z(), odomPoses[0].Rot().Z(), 1e-2);
190+
EXPECT_NEAR(poses.back().Pos().X(), finalModelFramePose.Pos().X(), 1e-2);
191+
EXPECT_NEAR(poses.back().Pos().Y(), finalModelFramePose.Pos().Y(), 1e-2);
192+
EXPECT_NEAR(poses.back().Pos().Z(), finalModelFramePose.Pos().Z(), 1e-2);
193+
EXPECT_NEAR(poses.back().Rot().X(), finalModelFramePose.Rot().X(), 1e-2);
194+
EXPECT_NEAR(poses.back().Rot().Y(), finalModelFramePose.Rot().Y(), 1e-2);
195+
EXPECT_NEAR(poses.back().Rot().Z(), finalModelFramePose.Rot().Z(), 1e-2);
196+
197+
// Check accuracy of velocities published in the odometry message
198+
EXPECT_NEAR(odomLinVels[5].X(), linVelCmd[0], 1e-1);
199+
EXPECT_NEAR(odomLinVels[5].Y(), linVelCmd[1], 1e-1);
200+
EXPECT_NEAR(odomLinVels[5].Z(), 0.0, 1e-1);
201+
EXPECT_NEAR(odomAngVels[5].X(), 0.0, 1e-1);
202+
EXPECT_NEAR(odomAngVels[5].Y(), 0.0, 1e-1);
203+
EXPECT_NEAR(odomAngVels[5].Z(), angVelCmd[2], 1e-1);
204+
205+
EXPECT_NEAR(odomLinVels.back().X(), linVelCmd[0], 1e-1);
206+
EXPECT_NEAR(odomLinVels.back().Y(), linVelCmd[1], 1e-1);
207+
EXPECT_NEAR(odomLinVels.back().Z(), 0.0, 1e-1);
208+
EXPECT_NEAR(odomAngVels.back().X(), 0.0, 1e-1);
209+
EXPECT_NEAR(odomAngVels.back().Y(), 0.0, 1e-1);
210+
EXPECT_NEAR(odomAngVels.back().Z(), angVelCmd[2], 1e-1);
211+
}
212+
213+
/// \param[in] _sdfFile SDF file to load.
214+
/// \param[in] _odomTopic Odometry topic.
215+
/// \param[in] _frameId Name of the world-fixed coordinate frame
216+
/// for the odometry message.
217+
/// \param[in] _childFrameId Name of the coordinate frame rigidly
218+
/// attached to the mobile robot base.
219+
protected: void TestPublishCmd(const std::string &_sdfFile,
220+
const std::string &_odomTopic,
221+
const std::string &_frameId,
222+
const std::string &_childFrameId)
223+
{
224+
// Start server
225+
ServerConfig serverConfig;
226+
serverConfig.SetSdfFile(_sdfFile);
227+
228+
Server server(serverConfig);
229+
EXPECT_FALSE(server.Running());
230+
EXPECT_FALSE(*server.Running(0));
231+
232+
server.SetUpdatePeriod(0ns);
233+
234+
unsigned int odomPosesCount = 0;
235+
std::function<void(const msgs::Odometry &)> odomCb =
236+
[&odomPosesCount, &_frameId, &_childFrameId](const msgs::Odometry &_msg)
237+
{
238+
ASSERT_TRUE(_msg.has_header());
239+
ASSERT_TRUE(_msg.header().has_stamp());
240+
241+
ASSERT_GT(_msg.header().data_size(), 1);
242+
243+
EXPECT_STREQ(_msg.header().data(0).key().c_str(), "frame_id");
244+
EXPECT_STREQ(
245+
_msg.header().data(0).value().Get(0).c_str(),
246+
_frameId.c_str());
247+
248+
EXPECT_STREQ(_msg.header().data(1).key().c_str(), "child_frame_id");
249+
EXPECT_STREQ(
250+
_msg.header().data(1).value().Get(0).c_str(),
251+
_childFrameId.c_str());
252+
253+
odomPosesCount++;
254+
};
255+
256+
transport::Node node;
257+
node.Subscribe(_odomTopic, odomCb);
258+
259+
server.Run(true, 100, false);
260+
261+
int sleep = 0;
262+
int maxSleep = 30;
263+
for (; odomPosesCount < 5 && sleep < maxSleep; ++sleep)
264+
{
265+
std::this_thread::sleep_for(std::chrono::milliseconds(100));
266+
}
267+
ASSERT_NE(maxSleep, sleep);
268+
269+
EXPECT_EQ(5u, odomPosesCount);
270+
}
271+
};
272+
273+
/////////////////////////////////////////////////
274+
TEST_P(OdometryPublisherTest, Movement)
275+
{
276+
TestMovement(
277+
std::string(PROJECT_SOURCE_PATH) + "/test/worlds/odometry_publisher.sdf",
278+
"/model/vehicle/odometry");
279+
}
280+
281+
/////////////////////////////////////////////////
282+
TEST_P(OdometryPublisherTest, MovementCustomTopic)
283+
{
284+
TestMovement(
285+
std::string(PROJECT_SOURCE_PATH) +
286+
"/test/worlds/odometry_publisher_custom.sdf",
287+
"/model/bar/odom");
288+
}
289+
290+
/////////////////////////////////////////////////
291+
TEST_P(OdometryPublisherTest, OdomFrameId)
292+
{
293+
TestPublishCmd(
294+
std::string(PROJECT_SOURCE_PATH) + "/test/worlds/odometry_publisher.sdf",
295+
"/model/vehicle/odometry",
296+
"vehicle/odom",
297+
"vehicle/base_footprint");
298+
}
299+
300+
/////////////////////////////////////////////////
301+
TEST_P(OdometryPublisherTest, OdomCustomFrameId)
302+
{
303+
TestPublishCmd(
304+
std::string(PROJECT_SOURCE_PATH) +
305+
"/test/worlds/odometry_publisher_custom.sdf",
306+
"/model/bar/odom",
307+
"odomCustom",
308+
"baseCustom");
309+
}
310+
311+
// Run multiple times
312+
INSTANTIATE_TEST_SUITE_P(ServerRepeat, OdometryPublisherTest,
313+
::testing::Range(1, 2));

0 commit comments

Comments
 (0)