16 #include "nav2_loopback_sim/loopback_simulator.hpp"
26 #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp"
28 using namespace std::chrono_literals;
29 using std::placeholders::_1;
31 namespace nav2_loopback_sim
34 LoopbackSimulator::LoopbackSimulator(
const rclcpp::NodeOptions & options)
35 : nav2::LifecycleNode(
"loopback_simulator", options),
36 curr_cmd_vel_time_(this->now())
43 RCLCPP_INFO(get_logger(),
"Configuring");
66 t_map_to_odom_.header.frame_id = map_frame_id_;
67 t_map_to_odom_.child_frame_id = odom_frame_id_;
68 t_odom_to_base_link_.header.frame_id = odom_frame_id_;
69 t_odom_to_base_link_.child_frame_id = base_frame_id_;
71 tf_broadcaster_ = nav2::create_transform_broadcaster(
this);
75 create_subscription<geometry_msgs::msg::PoseWithCovarianceStamped>(
79 cmd_vel_sub_ = std::make_unique<nav2_util::TwistSubscriber>(
85 odom_pub_ = create_publisher<nav_msgs::msg::Odometry>(
"odom");
88 scan_pub_ = create_publisher<sensor_msgs::msg::LaserScan>(
93 map_client_ = create_client<nav_msgs::srv::GetMap>(
"/map_server/map");
94 tf_buffer_ = nav2::create_transform_buffer(
this);
95 tf_listener_ = nav2::create_transform_listener(*tf_buffer_);
99 clock_publisher_ = std::make_unique<ClockPublisher>(
104 param_validator_ = add_on_set_parameters_callback(
107 std::placeholders::_1));
108 param_updater_ = add_post_set_parameters_callback(
111 std::placeholders::_1));
113 return nav2::CallbackReturn::SUCCESS;
119 RCLCPP_INFO(get_logger(),
"Activating");
121 odom_pub_->on_activate();
123 scan_pub_->on_activate();
130 if (clock_publisher_) {
131 clock_publisher_->start();
136 RCLCPP_INFO(get_logger(),
"Loopback simulator activated");
137 return nav2::CallbackReturn::SUCCESS;
143 RCLCPP_INFO(get_logger(),
"Deactivating");
146 setup_timer_->cancel();
147 setup_timer_.reset();
154 odom_timer_->cancel();
158 scan_timer_->cancel();
162 if (clock_publisher_) {
163 clock_publisher_->stop();
166 odom_pub_->on_deactivate();
168 scan_pub_->on_deactivate();
171 has_initial_pose_ =
false;
172 curr_cmd_vel_.reset();
176 return nav2::CallbackReturn::SUCCESS;
182 RCLCPP_INFO(get_logger(),
"Cleaning up");
184 initial_pose_sub_.reset();
185 cmd_vel_sub_.reset();
189 tf_listener_.reset();
191 tf_broadcaster_.reset();
192 clock_publisher_.reset();
193 param_validator_.reset();
194 param_updater_.reset();
196 return nav2::CallbackReturn::SUCCESS;
202 RCLCPP_INFO(get_logger(),
"Shutting down");
203 return nav2::CallbackReturn::SUCCESS;
211 auto request = std::make_shared<nav_msgs::srv::GetMap::Request>();
214 [
this](
typename rclcpp::Client<nav_msgs::srv::GetMap>::SharedFuture future) {
215 auto response = future.get();
216 if (response->map.info.width == 0 || response->map.info.height == 0 ||
217 response->map.info.resolution <= 0.0)
221 "Map server returned empty/invalid map (%dx%d, res=%.3f), will retry",
222 response->map.info.width, response->map.info.height,
223 response->map.info.resolution);
226 map_ = response->map;
228 RCLCPP_INFO(get_logger(),
"Laser scan will be populated using map data");
235 auto transform = tf_buffer_->lookupTransform(
236 base_frame_id_, scan_frame_id_, tf2::TimePointZero);
237 tf2::fromMsg(transform.transform, tf_base_to_laser_);
238 has_base_to_laser_ =
true;
239 }
catch (
const tf2::TransformException & ex) {
240 RCLCPP_ERROR(get_logger(),
"Transform lookup failed: %s", ex.what());
246 t_odom_to_base_link_.header.stamp = this->now();
247 tf_broadcaster_->sendTransform(t_odom_to_base_link_);
248 if (publish_scan_ && !has_map_) {
251 if (publish_scan_ && !has_base_to_laser_) {
257 const geometry_msgs::msg::Twist::ConstSharedPtr & msg)
259 RCLCPP_DEBUG(get_logger(),
"Received cmd_vel");
260 if (!has_initial_pose_) {
263 curr_cmd_vel_ = *msg;
264 curr_cmd_vel_time_ = this->now();
268 const geometry_msgs::msg::TwistStamped::ConstSharedPtr & msg)
270 RCLCPP_DEBUG(get_logger(),
"Received cmd_vel");
271 if (!has_initial_pose_) {
274 curr_cmd_vel_ = msg->twist;
275 curr_cmd_vel_time_ = rclcpp::Time(msg->header.stamp);
279 const geometry_msgs::msg::PoseWithCovarianceStamped::ConstSharedPtr & msg)
281 RCLCPP_INFO(get_logger(),
"Received initial pose!");
283 if (!has_initial_pose_) {
284 has_initial_pose_ =
true;
285 initial_pose_ = msg->pose.pose;
288 t_map_to_odom_.transform.translation.x = initial_pose_.position.x;
289 t_map_to_odom_.transform.translation.y = initial_pose_.position.y;
290 t_map_to_odom_.transform.rotation = initial_pose_.orientation;
291 t_odom_to_base_link_.transform.translation = geometry_msgs::msg::Vector3();
292 t_odom_to_base_link_.transform.rotation = geometry_msgs::msg::Quaternion();
297 setup_timer_->cancel();
298 setup_timer_.reset();
301 std::chrono::duration<double>(update_dur_),
304 std::chrono::duration<double>(odom_publish_dur_),
308 std::chrono::duration<double>(scan_publish_dur_),
314 initial_pose_ = msg->pose.pose;
317 tf2::Transform tf_map_to_base;
318 tf_map_to_base.setOrigin(
319 tf2::Vector3(initial_pose_.position.x, initial_pose_.position.y, 0.0));
320 tf_map_to_base.setRotation(
322 initial_pose_.orientation.x, initial_pose_.orientation.y,
323 initial_pose_.orientation.z, initial_pose_.orientation.w));
325 tf2::Transform tf_odom_to_base;
326 tf2::fromMsg(t_odom_to_base_link_.transform, tf_odom_to_base);
328 tf2::Transform tf_map_to_odom = tf_map_to_base * tf_odom_to_base.inverse();
329 t_map_to_odom_.transform = tf2::toMsg(tf_map_to_odom);
335 auto one_sec = rclcpp::Duration::from_seconds(1.0);
336 if (!curr_cmd_vel_.has_value() || (this->now() - curr_cmd_vel_time_) > one_sec) {
338 curr_cmd_vel_.reset();
343 double dx = curr_cmd_vel_->linear.x * update_dur_;
344 double dy = curr_cmd_vel_->linear.y * update_dur_;
345 double dth = curr_cmd_vel_->angular.z * update_dur_;
348 t_odom_to_base_link_.transform.rotation.x,
349 t_odom_to_base_link_.transform.rotation.y,
350 t_odom_to_base_link_.transform.rotation.z,
351 t_odom_to_base_link_.transform.rotation.w);
352 double roll, pitch, yaw;
353 tf2::Matrix3x3(q).getRPY(roll, pitch, yaw);
355 t_odom_to_base_link_.transform.translation.x += dx * std::cos(yaw) - dy * std::sin(yaw);
356 t_odom_to_base_link_.transform.translation.y += dx * std::sin(yaw) + dy * std::cos(yaw);
357 t_odom_to_base_link_.transform.rotation =
358 addYawToQuat(t_odom_to_base_link_.transform.rotation, dth);
370 auto scan_msg = std::make_unique<sensor_msgs::msg::LaserScan>();
371 scan_msg->header.stamp = this->now();
372 scan_msg->header.frame_id = scan_frame_id_;
373 scan_msg->angle_min =
static_cast<float>(scan_angle_min_);
374 scan_msg->angle_max =
static_cast<float>(scan_angle_max_);
375 scan_msg->angle_increment =
static_cast<float>(scan_angle_increment_);
376 scan_msg->time_increment = 0.0f;
377 scan_msg->scan_time =
static_cast<float>(scan_publish_dur_);
378 scan_msg->range_min =
static_cast<float>(scan_range_min_);
379 scan_msg->range_max =
static_cast<float>(scan_range_max_);
381 int num_samples =
static_cast<int>(
382 (scan_angle_max_ - scan_angle_min_) / scan_angle_increment_);
383 scan_msg->ranges.assign(num_samples, 0.0f);
387 if (!has_base_to_laser_) {
391 scan_pub_->publish(std::move(scan_msg));
395 geometry_msgs::msg::TransformStamped & map_to_odom,
396 geometry_msgs::msg::TransformStamped & odom_to_base_link)
398 auto now = this->now();
399 map_to_odom.header.stamp = now + rclcpp::Duration::from_seconds(update_dur_);
400 odom_to_base_link.header.stamp = now;
401 if (publish_map_odom_tf_) {
402 tf_broadcaster_->sendTransform(map_to_odom);
404 tf_broadcaster_->sendTransform(odom_to_base_link);
408 const geometry_msgs::msg::TransformStamped & odom_to_base_link)
410 auto odom = std::make_unique<nav_msgs::msg::Odometry>();
411 odom->header.stamp = this->now();
412 odom->header.frame_id = odom_frame_id_;
413 odom->child_frame_id = base_frame_id_;
414 odom->pose.pose.position.x = odom_to_base_link.transform.translation.x;
415 odom->pose.pose.position.y = odom_to_base_link.transform.translation.y;
416 odom->pose.pose.orientation = odom_to_base_link.transform.rotation;
417 if (curr_cmd_vel_.has_value()) {
418 odom->twist.twist = curr_cmd_vel_.value();
420 odom_pub_->publish(std::move(odom));
424 const geometry_msgs::msg::Quaternion & quaternion,
double yaw_to_add)
426 tf2::Quaternion q(quaternion.x, quaternion.y, quaternion.z, quaternion.w);
427 tf2::Quaternion q_yaw;
428 q_yaw.setRPY(0.0, 0.0, yaw_to_add);
431 return tf2::toMsg(q);
436 tf2::Transform tf_map_to_odom;
437 tf2::fromMsg(t_map_to_odom_.transform, tf_map_to_odom);
439 tf2::Transform tf_odom_to_base;
440 tf2::fromMsg(t_odom_to_base_link_.transform, tf_odom_to_base);
442 tf2::Transform tf_map_to_laser = tf_map_to_odom * tf_odom_to_base * tf_base_to_laser_;
444 double x = tf_map_to_laser.getOrigin().x();
445 double y = tf_map_to_laser.getOrigin().y();
446 double roll, pitch, yaw;
447 tf2::Matrix3x3(tf_map_to_laser.getRotation()).getRPY(roll, pitch, yaw);
453 int num_samples, sensor_msgs::msg::LaserScan & scan_msg)
455 float no_hit_range = use_inf_ ? std::numeric_limits<float>::infinity() :
456 scan_msg.range_max - 0.1f;
458 if (!has_map_ || !has_initial_pose_ || !has_base_to_laser_) {
459 scan_msg.ranges.assign(num_samples, no_hit_range);
465 double resolution = map_.info.resolution;
466 double origin_x = map_.info.origin.position.x;
467 double origin_y = map_.info.origin.position.y;
468 int width =
static_cast<int>(map_.info.width);
469 int height =
static_cast<int>(map_.info.height);
471 int mx0 =
static_cast<int>(std::floor((x0 - origin_x) / resolution));
472 int my0 =
static_cast<int>(std::floor((y0 - origin_y) / resolution));
474 if (mx0 <= 0 || mx0 >= width || my0 <= 0 || my0 >= height) {
475 scan_msg.ranges.assign(num_samples, no_hit_range);
479 const auto & map_data = map_.data;
480 double range_max = scan_msg.range_max;
481 double angle_min = scan_msg.angle_min;
482 double angle_increment = scan_msg.angle_increment;
483 double step = resolution * 0.5;
485 for (
int i = 0; i < num_samples; ++i) {
486 double angle = theta + angle_min + i * angle_increment;
487 double cos_a = std::cos(angle);
488 double sin_a = std::sin(angle);
489 scan_msg.ranges[i] = no_hit_range;
491 for (
double d = 0.0; d <= range_max; d += step) {
492 int mx =
static_cast<int>(std::floor((x0 + d * cos_a - origin_x) / resolution));
493 int my =
static_cast<int>(std::floor((y0 + d * sin_a - origin_y) / resolution));
494 if (mx <= 0 || mx >= width || my <= 0 || my >= height) {
497 if (map_data[my * width + mx] >= 60) {
498 scan_msg.ranges[i] =
static_cast<float>(d);
505 if (scan_noise_std_ > 0.0) {
506 std::normal_distribution<float> noise(0.0f,
static_cast<float>(scan_noise_std_));
507 for (
int i = 0; i < num_samples; ++i) {
508 float & r = scan_msg.ranges[i];
509 if (std::isfinite(r) && r > 0.0f) {
510 r = std::max(0.0f, r + noise(rng_));
516 rcl_interfaces::msg::SetParametersResult
518 const std::vector<rclcpp::Parameter> & parameters)
520 rcl_interfaces::msg::SetParametersResult result;
521 result.successful =
true;
522 for (
const auto & param : parameters) {
523 if (param.get_name() ==
"speed_factor") {
524 double factor = param.as_double();
526 result.successful =
false;
527 result.reason =
"speed_factor must be positive";
536 const std::vector<rclcpp::Parameter> & parameters)
538 for (
const auto & param : parameters) {
539 if (param.get_name() ==
"speed_factor") {
540 speed_factor_ = param.as_double();
541 if (clock_publisher_) {
542 clock_publisher_->setSpeedFactor(speed_factor_);
550 #include "rclcpp_components/register_node_macro.hpp"
void destroyBond()
Destroy bond connection to lifecycle manager.
nav2::LifecycleNode::SharedPtr shared_from_this()
Get a shared pointer of this.
rclcpp::GenericTimer< CallbackT >::SharedPtr create_timer(std::chrono::duration< DurationRepT, DurationT > period, CallbackT callback, rclcpp::CallbackGroup::SharedPtr group=nullptr)
Create a sim-time-aware timer for Nav2 lifecycle nodes.
ParameterT declare_or_get_parameter(const std::string ¶meter_name, const ParameterDescriptor ¶meter_descriptor=ParameterDescriptor())
Declares or gets a parameter with specified type (not value). If the parameter is already declared,...
void createBond()
Create bond connection to lifecycle manager.
nav2::LifecycleNode::WeakPtr weak_from_this()
Get a shared pointer of this.
std::shared_future< typename ResponseType::SharedPtr > async_call(typename RequestType::SharedPtr &request)
Asynchronously call the service.
bool wait_for_service(const std::chrono::nanoseconds timeout=std::chrono::nanoseconds::max())
Block until a service is available or timeout.
A QoS profile for best-effort sensor data with a history of 10 messages.
A loopback simulator that replaces a physics simulator to create a frictionless, inertialess,...
void odomTimerCallback()
Periodic odometry publishing callback.
void cmdVelCallback(const geometry_msgs::msg::Twist::ConstSharedPtr &msg)
Callback for incoming cmd_vel (unstamped Twist)
rcl_interfaces::msg::SetParametersResult validateParameterUpdatesCallback(const std::vector< rclcpp::Parameter > ¶meters)
Validate dynamic parameter changes (pre-set callback)
std::tuple< double, double, double > getLaserPose()
Compute the laser pose in the map frame.
void publishTransforms(geometry_msgs::msg::TransformStamped &map_to_odom, geometry_msgs::msg::TransformStamped &odom_to_base_link)
Publish map->odom and odom->base_link transforms.
void publishOdometry(const geometry_msgs::msg::TransformStamped &odom_to_base_link)
Publish nav_msgs::Odometry from the current odom->base transform.
static geometry_msgs::msg::Quaternion addYawToQuat(const geometry_msgs::msg::Quaternion &quaternion, double yaw_to_add)
Add a yaw rotation to a quaternion.
void getMap()
Request the map from the map server.
void cmdVelStampedCallback(const geometry_msgs::msg::TwistStamped::ConstSharedPtr &msg)
Callback for incoming cmd_vel (stamped TwistStamped)
void updateParametersCallback(const std::vector< rclcpp::Parameter > ¶meters)
Apply validated dynamic parameter changes (post-set callback)
nav2::CallbackReturn on_configure(const rclcpp_lifecycle::State &state) override
Configure the node: declare parameters, create pubs/subs/timers.
void timerCallback()
Main update callback: integrates cmd_vel and publishes TF.
void initialPoseCallback(const geometry_msgs::msg::PoseWithCovarianceStamped::ConstSharedPtr &msg)
Callback for incoming initial pose.
nav2::CallbackReturn on_cleanup(const rclcpp_lifecycle::State &state) override
Cleanup the node: release all resources.
void publishLaserScan()
Publish a simulated laser scan from the map.
nav2::CallbackReturn on_deactivate(const rclcpp_lifecycle::State &state) override
Deactivate the node: stop timers, reset cmd_vel.
void getLaserScan(int num_samples, sensor_msgs::msg::LaserScan &scan_msg)
Raycast the map to fill a LaserScan message.
void setupTimerCallback()
Periodic setup callback: publishes identity TFs and fetches map.
nav2::CallbackReturn on_activate(const rclcpp_lifecycle::State &state) override
Activate the node: start publishing.
void getBaseToLaserTf()
Look up the static transform from base to laser frame.
nav2::CallbackReturn on_shutdown(const rclcpp_lifecycle::State &state) override
Shutdown the node.