Nav2 Navigation Stack - lyrical  lyrical
ROS 2 Navigation Stack
loopback_simulator.cpp
1 // Copyright (c) 2024, Open Navigation LLC
2 // Copyright (c) 2026, Dexory (Tony Najjar)
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 #include "nav2_loopback_sim/loopback_simulator.hpp"
17 
18 #include <cmath>
19 #include <limits>
20 #include <memory>
21 #include <random>
22 #include <string>
23 #include <tuple>
24 #include <chrono>
25 
26 #include "tf2_geometry_msgs/tf2_geometry_msgs.hpp"
27 
28 using namespace std::chrono_literals;
29 using std::placeholders::_1;
30 
31 namespace nav2_loopback_sim
32 {
33 
34 LoopbackSimulator::LoopbackSimulator(const rclcpp::NodeOptions & options)
35 : nav2::LifecycleNode("loopback_simulator", options),
36  curr_cmd_vel_time_(this->now())
37 {
38 }
39 
40 nav2::CallbackReturn
41 LoopbackSimulator::on_configure(const rclcpp_lifecycle::State & /*state*/)
42 {
43  RCLCPP_INFO(get_logger(), "Configuring");
44 
45  // Declare and get parameters
46  update_dur_ = declare_or_get_parameter("update_duration", 0.01);
47  base_frame_id_ = declare_or_get_parameter("base_frame_id", std::string("base_footprint"));
48  map_frame_id_ = declare_or_get_parameter("map_frame_id", std::string("map"));
49  odom_frame_id_ = declare_or_get_parameter("odom_frame_id", std::string("odom"));
50  scan_frame_id_ = declare_or_get_parameter("scan_frame_id", std::string("base_scan"));
51  odom_publish_dur_ = declare_or_get_parameter("odom_publish_dur", update_dur_);
52  scan_publish_dur_ = declare_or_get_parameter("scan_publish_dur", 0.1);
53  publish_map_odom_tf_ = declare_or_get_parameter("publish_map_odom_tf", true);
54  scan_range_min_ = declare_or_get_parameter("scan_range_min", 0.05);
55  scan_range_max_ = declare_or_get_parameter("scan_range_max", 30.0);
56  scan_angle_min_ = declare_or_get_parameter("scan_angle_min", -M_PI);
57  scan_angle_max_ = declare_or_get_parameter("scan_angle_max", M_PI);
58  scan_angle_increment_ = declare_or_get_parameter("scan_angle_increment", 0.0261);
59  use_inf_ = declare_or_get_parameter("scan_use_inf", true);
60  scan_noise_std_ = declare_or_get_parameter("scan_noise_std", 0.01);
61  publish_scan_ = declare_or_get_parameter("publish_scan", true);
62  publish_clock_ = declare_or_get_parameter("publish_clock", true);
63  speed_factor_ = declare_or_get_parameter("speed_factor", 1.0);
64 
65  // Setup transforms
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_;
70 
71  tf_broadcaster_ = nav2::create_transform_broadcaster(this);
72 
73  // Subscriptions
74  initial_pose_sub_ =
75  create_subscription<geometry_msgs::msg::PoseWithCovarianceStamped>(
76  "initialpose",
77  std::bind(&LoopbackSimulator::initialPoseCallback, this, _1));
78 
79  cmd_vel_sub_ = std::make_unique<nav2_util::TwistSubscriber>(
80  shared_from_this(), "cmd_vel",
81  std::bind(&LoopbackSimulator::cmdVelCallback, this, _1),
82  std::bind(&LoopbackSimulator::cmdVelStampedCallback, this, _1));
83 
84  // Publishers
85  odom_pub_ = create_publisher<nav_msgs::msg::Odometry>("odom");
86 
87  if (publish_scan_) {
88  scan_pub_ = create_publisher<sensor_msgs::msg::LaserScan>(
89  "scan", nav2::qos::SensorDataQoS());
90  }
91 
92  if (publish_scan_) {
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_);
96  }
97 
98  if (publish_clock_) {
99  clock_publisher_ = std::make_unique<ClockPublisher>(
100  weak_from_this(),
101  speed_factor_);
102  }
103 
104  param_validator_ = add_on_set_parameters_callback(
105  std::bind(
107  std::placeholders::_1));
108  param_updater_ = add_post_set_parameters_callback(
109  std::bind(
111  std::placeholders::_1));
112 
113  return nav2::CallbackReturn::SUCCESS;
114 }
115 
116 nav2::CallbackReturn
117 LoopbackSimulator::on_activate(const rclcpp_lifecycle::State & /*state*/)
118 {
119  RCLCPP_INFO(get_logger(), "Activating");
120 
121  odom_pub_->on_activate();
122  if (scan_pub_) {
123  scan_pub_->on_activate();
124  }
125 
126  setup_timer_ = this->create_timer(
127  100ms,
128  std::bind(&LoopbackSimulator::setupTimerCallback, this));
129 
130  if (clock_publisher_) {
131  clock_publisher_->start();
132  }
133 
134  createBond();
135 
136  RCLCPP_INFO(get_logger(), "Loopback simulator activated");
137  return nav2::CallbackReturn::SUCCESS;
138 }
139 
140 nav2::CallbackReturn
141 LoopbackSimulator::on_deactivate(const rclcpp_lifecycle::State & /*state*/)
142 {
143  RCLCPP_INFO(get_logger(), "Deactivating");
144 
145  if (setup_timer_) {
146  setup_timer_->cancel();
147  setup_timer_.reset();
148  }
149  if (timer_) {
150  timer_->cancel();
151  timer_.reset();
152  }
153  if (odom_timer_) {
154  odom_timer_->cancel();
155  odom_timer_.reset();
156  }
157  if (scan_timer_) {
158  scan_timer_->cancel();
159  scan_timer_.reset();
160  }
161 
162  if (clock_publisher_) {
163  clock_publisher_->stop();
164  }
165 
166  odom_pub_->on_deactivate();
167  if (scan_pub_) {
168  scan_pub_->on_deactivate();
169  }
170 
171  has_initial_pose_ = false;
172  curr_cmd_vel_.reset();
173 
174  destroyBond();
175 
176  return nav2::CallbackReturn::SUCCESS;
177 }
178 
179 nav2::CallbackReturn
180 LoopbackSimulator::on_cleanup(const rclcpp_lifecycle::State & /*state*/)
181 {
182  RCLCPP_INFO(get_logger(), "Cleaning up");
183 
184  initial_pose_sub_.reset();
185  cmd_vel_sub_.reset();
186  odom_pub_.reset();
187  scan_pub_.reset();
188  map_client_.reset();
189  tf_listener_.reset();
190  tf_buffer_.reset();
191  tf_broadcaster_.reset();
192  clock_publisher_.reset();
193  param_validator_.reset();
194  param_updater_.reset();
195 
196  return nav2::CallbackReturn::SUCCESS;
197 }
198 
199 nav2::CallbackReturn
200 LoopbackSimulator::on_shutdown(const rclcpp_lifecycle::State & /*state*/)
201 {
202  RCLCPP_INFO(get_logger(), "Shutting down");
203  return nav2::CallbackReturn::SUCCESS;
204 }
205 
207 {
208  if (!map_client_->wait_for_service(0s)) {
209  return;
210  }
211  auto request = std::make_shared<nav_msgs::srv::GetMap::Request>();
212  map_client_->async_call(
213  request,
214  [this](typename rclcpp::Client<nav_msgs::srv::GetMap>::SharedFuture future) { // nosemgrep
215  auto response = future.get();
216  if (response->map.info.width == 0 || response->map.info.height == 0 ||
217  response->map.info.resolution <= 0.0)
218  {
219  RCLCPP_WARN(
220  get_logger(),
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);
224  return;
225  }
226  map_ = response->map;
227  has_map_ = true;
228  RCLCPP_INFO(get_logger(), "Laser scan will be populated using map data");
229  });
230 }
231 
233 {
234  try {
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());
241  }
242 }
243 
245 {
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_) {
249  getMap();
250  }
251  if (publish_scan_ && !has_base_to_laser_) {
253  }
254 }
255 
257  const geometry_msgs::msg::Twist::ConstSharedPtr & msg)
258 {
259  RCLCPP_DEBUG(get_logger(), "Received cmd_vel");
260  if (!has_initial_pose_) {
261  return;
262  }
263  curr_cmd_vel_ = *msg;
264  curr_cmd_vel_time_ = this->now();
265 }
266 
268  const geometry_msgs::msg::TwistStamped::ConstSharedPtr & msg)
269 {
270  RCLCPP_DEBUG(get_logger(), "Received cmd_vel");
271  if (!has_initial_pose_) {
272  return;
273  }
274  curr_cmd_vel_ = msg->twist;
275  curr_cmd_vel_time_ = rclcpp::Time(msg->header.stamp);
276 }
277 
279  const geometry_msgs::msg::PoseWithCovarianceStamped::ConstSharedPtr & msg)
280 {
281  RCLCPP_INFO(get_logger(), "Received initial pose!");
282 
283  if (!has_initial_pose_) {
284  has_initial_pose_ = true;
285  initial_pose_ = msg->pose.pose;
286 
287  // Initialize map->odom from input pose, odom->base_link starts as identity
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();
293  publishTransforms(t_map_to_odom_, t_odom_to_base_link_);
294 
295  // Cancel setup timer and start update/scan timers
296  if (setup_timer_) {
297  setup_timer_->cancel();
298  setup_timer_.reset();
299  }
300  timer_ = this->create_timer(
301  std::chrono::duration<double>(update_dur_),
302  std::bind(&LoopbackSimulator::timerCallback, this));
303  odom_timer_ = this->create_timer(
304  std::chrono::duration<double>(odom_publish_dur_),
305  std::bind(&LoopbackSimulator::odomTimerCallback, this));
306  if (publish_scan_) {
307  scan_timer_ = this->create_timer(
308  std::chrono::duration<double>(scan_publish_dur_),
309  std::bind(&LoopbackSimulator::publishLaserScan, this));
310  }
311  return;
312  }
313 
314  initial_pose_ = msg->pose.pose;
315 
316  // Adjust map->odom based on new initial pose, keeping odom->base_link the same
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(
321  tf2::Quaternion(
322  initial_pose_.orientation.x, initial_pose_.orientation.y,
323  initial_pose_.orientation.z, initial_pose_.orientation.w));
324 
325  tf2::Transform tf_odom_to_base;
326  tf2::fromMsg(t_odom_to_base_link_.transform, tf_odom_to_base);
327 
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);
330 }
331 
333 {
334  // If no data, just republish existing transforms without change
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) {
337  publishTransforms(t_map_to_odom_, t_odom_to_base_link_);
338  curr_cmd_vel_.reset();
339  return;
340  }
341 
342  // Update odom->base_link from cmd_vel
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_;
346 
347  tf2::Quaternion q(
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);
354 
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);
359 
360  publishTransforms(t_map_to_odom_, t_odom_to_base_link_);
361 }
362 
364 {
365  publishOdometry(t_odom_to_base_link_);
366 }
367 
369 {
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_);
380 
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);
384  if (!has_map_) {
385  getMap();
386  }
387  if (!has_base_to_laser_) {
389  }
390  getLaserScan(num_samples, *scan_msg);
391  scan_pub_->publish(std::move(scan_msg));
392 }
393 
395  geometry_msgs::msg::TransformStamped & map_to_odom,
396  geometry_msgs::msg::TransformStamped & odom_to_base_link)
397 {
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);
403  }
404  tf_broadcaster_->sendTransform(odom_to_base_link);
405 }
406 
408  const geometry_msgs::msg::TransformStamped & odom_to_base_link)
409 {
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();
419  }
420  odom_pub_->publish(std::move(odom));
421 }
422 
423 geometry_msgs::msg::Quaternion LoopbackSimulator::addYawToQuat(
424  const geometry_msgs::msg::Quaternion & quaternion, double yaw_to_add)
425 {
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);
429  q = q * q_yaw;
430  q.normalize();
431  return tf2::toMsg(q);
432 }
433 
434 std::tuple<double, double, double> LoopbackSimulator::getLaserPose()
435 {
436  tf2::Transform tf_map_to_odom;
437  tf2::fromMsg(t_map_to_odom_.transform, tf_map_to_odom);
438 
439  tf2::Transform tf_odom_to_base;
440  tf2::fromMsg(t_odom_to_base_link_.transform, tf_odom_to_base);
441 
442  tf2::Transform tf_map_to_laser = tf_map_to_odom * tf_odom_to_base * tf_base_to_laser_;
443 
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);
448 
449  return {x, y, yaw};
450 }
451 
453  int num_samples, sensor_msgs::msg::LaserScan & scan_msg)
454 {
455  float no_hit_range = use_inf_ ? std::numeric_limits<float>::infinity() :
456  scan_msg.range_max - 0.1f;
457 
458  if (!has_map_ || !has_initial_pose_ || !has_base_to_laser_) {
459  scan_msg.ranges.assign(num_samples, no_hit_range);
460  return;
461  }
462 
463  auto [x0, y0, theta] = getLaserPose();
464 
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);
470 
471  int mx0 = static_cast<int>(std::floor((x0 - origin_x) / resolution));
472  int my0 = static_cast<int>(std::floor((y0 - origin_y) / resolution));
473 
474  if (mx0 <= 0 || mx0 >= width || my0 <= 0 || my0 >= height) {
475  scan_msg.ranges.assign(num_samples, no_hit_range);
476  return;
477  }
478 
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;
484 
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;
490 
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) {
495  break;
496  }
497  if (map_data[my * width + mx] >= 60) {
498  scan_msg.ranges[i] = static_cast<float>(d);
499  break;
500  }
501  }
502  }
503 
504  // Add Gaussian noise to valid range measurements
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_));
511  }
512  }
513  }
514 }
515 
516 rcl_interfaces::msg::SetParametersResult
518  const std::vector<rclcpp::Parameter> & parameters)
519 {
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();
525  if (factor <= 0.0) {
526  result.successful = false;
527  result.reason = "speed_factor must be positive";
528  return result;
529  }
530  }
531  }
532  return result;
533 }
534 
536  const std::vector<rclcpp::Parameter> & parameters)
537 {
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_);
543  }
544  }
545  }
546 }
547 
548 } // namespace nav2_loopback_sim
549 
550 #include "rclcpp_components/register_node_macro.hpp"
551 RCLCPP_COMPONENTS_REGISTER_NODE(nav2_loopback_sim::LoopbackSimulator)
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 &parameter_name, const ParameterDescriptor &parameter_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 > &parameters)
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 > &parameters)
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.