Nav2 Navigation Stack - lyrical  lyrical
ROS 2 Navigation Stack
route_tracker.cpp
1 // Copyright (c) 2025, Open Navigation LLC
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 #include "nav2_route/route_tracker.hpp"
16 
17 #include "nav2_ros_common/rate.hpp"
18 #include "nav2_ros_common/tf2_factories.hpp"
19 
20 namespace nav2_route
21 {
22 
24  nav2::LifecycleNode::SharedPtr node,
25  nav2::TransformBuffer::SharedPtr tf_buffer,
26  std::shared_ptr<nav2_costmap_2d::CostmapSubscriber> costmap_subscriber,
27  std::shared_ptr<ActionServerTrack> action_server,
28  const std::string & route_frame,
29  const std::string & base_frame)
30 {
31  node_ = node;
32  clock_ = node->get_clock();
33  logger_ = node->get_logger();
34  route_frame_ = route_frame;
35  base_frame_ = base_frame;
36  action_server_ = action_server;
37  tf_buffer_ = tf_buffer;
38 
39  radius_threshold_ = node->declare_or_get_parameter("radius_to_achieve_node", 2.0);
40  boundary_radius_threshold_ = node->declare_or_get_parameter(
41  "boundary_radius_to_achieve_node", 1.0);
42  tracker_update_rate_ = node->declare_or_get_parameter("tracker_update_rate", 50.0);
43  aggregate_blocked_ids_ = node->declare_or_get_parameter(
44  "aggregate_blocked_ids", false);
45 
46  operations_manager_ = std::make_unique<OperationsManager>(node, costmap_subscriber);
47 }
48 
49 geometry_msgs::msg::PoseStamped RouteTracker::getRobotPose()
50 {
51  geometry_msgs::msg::PoseStamped pose;
52  if (!nav2_util::getCurrentPose(pose, *tf_buffer_, route_frame_, base_frame_)) {
53  throw nav2_core::RouteTFError("Unable to get robot pose in route frame: " + route_frame_);
54  }
55  return pose;
56 }
57 
59  const geometry_msgs::msg::PoseStamped & pose,
60  RouteTrackingState & state,
61  const Route & route)
62 {
63  // check if inside a *generous* radius window
64  const double dx = state.next_node->coords.x - pose.pose.position.x;
65  const double dy = state.next_node->coords.y - pose.pose.position.y;
66  const double dist_mag = std::sqrt(dx * dx + dy * dy);
67  const bool is_boundary_node = isStartOrEndNode(state, route);
68  const bool in_radius =
69  (dist_mag <= (is_boundary_node ? boundary_radius_threshold_ : radius_threshold_));
70 
71  // Within 0.1mm is achieved or within radius and now not, consider node achieved
72  if (dist_mag < 1e-4 || (!in_radius && state.within_radius)) {
73  return true;
74  }
75 
76  // Update the state for the next iteration
77  state.within_radius = in_radius;
78 
79  // If start or end node, use the radius check only since the final node may not pass
80  // threshold depending on the configurations. The start node has no last_node for
81  // computing the vector bisector. If this is an issue, please file a ticket to discuss.
82  if (is_boundary_node) {
83  return state.within_radius;
84  }
85 
86  // We can evaluate the unit distance vector from the node w.r.t. the unit vector bisecting
87  // the last and current edges to find the average whose orthogonal is an imaginary
88  // line representing the migration from one edge's spatial domain to the other.
89  // When the dot product is negative, it means that there exists a projection between
90  // the vectors and that the robot position has passed this imaginary orthogonal line.
91  // This enables a more refined definition of when a node is considered achieved while
92  // enabling the use of dynamic behavior that may deviate from the path non-trivially
93  if (state.within_radius) {
94  NodePtr last_node = state.current_edge->start;
95  const double nx = state.next_node->coords.x - last_node->coords.x;
96  const double ny = state.next_node->coords.y - last_node->coords.y;
97  const double n_mag = std::sqrt(nx * nx + ny * ny);
98 
99  NodePtr future_next_node = route.edges[state.route_edges_idx + 1]->end;
100  const double mx = future_next_node->coords.x - state.next_node->coords.x;
101  const double my = future_next_node->coords.y - state.next_node->coords.y;
102  const double m_mag = std::sqrt(mx * mx + my * my);
103 
104  // If nodes overlap so there is no vector, use radius check only (divide by zero)
105  if (n_mag < 1e-6 || m_mag < 1e-6) {
106  return true;
107  }
108 
109  // Unnormalized Bisector = |n|*m + |m|*n
110  const double bx = nx * m_mag + mx * n_mag;
111  const double by = ny * m_mag + my * n_mag;
112  return utils::normalizedDot(bx, by, dx, dy) <= 0;
113  }
114 
115  return false;
116 }
117 
119 {
120  // Check if current_edge is nullptr in case we have a rerouted previous
121  // edge to use for the refined node achievement vectorized estimate
122  return
123  (state.route_edges_idx == static_cast<int>(route.edges.size() - 1)) ||
124  (state.route_edges_idx == -1 && !state.current_edge);
125 }
126 
128  const bool rereouted,
129  const unsigned int next_node_id,
130  const unsigned int last_node_id,
131  const unsigned int edge_id,
132  const std::vector<std::string> & operations)
133 {
134  auto feedback = std::make_unique<Feedback>();
135  feedback->route = route_msg_;
136  feedback->path = path_;
137  feedback->rerouted = rereouted;
138  feedback->next_node_id = next_node_id;
139  feedback->last_node_id = last_node_id;
140  feedback->current_edge_id = edge_id;
141  feedback->operations_triggered = operations;
142  action_server_->publish_feedback(std::move(feedback));
143 }
144 
146  const Route & route, const nav_msgs::msg::Path & path,
147  ReroutingState & rerouting_info)
148 {
149  route_msg_ = utils::toMsg(route, route_frame_, clock_->now());
150  path_ = path;
151  RouteTrackingState state;
152  state.next_node = route.start_node;
153 
154  // If we're rerouted but still covering the same previous edge to
155  // start, retain the state so we can continue as previously set with
156  // refined node achievement logic and performing edge operations on exit
157  if (rerouting_info.curr_edge) {
158  // state.next_node is not updated since the first edge is removed from route when rerouted
159  // along the same edge in the goal intent extractor. Thus, state.next_node is still the
160  // future node to reach in this case and we add in the state.last_node and state.current_edge
161  // to represent the 'currently' progressing edge that is omitted from the route (and its start)
162  state.current_edge = rerouting_info.curr_edge;
163  state.last_node = state.current_edge->start;
165  true, route.start_node->nodeid, state.last_node->nodeid, state.current_edge->edgeid, {});
166  } else {
167  publishFeedback(true, route.start_node->nodeid, 0, 0, {});
168  }
169 
170  auto node = node_.lock();
171  if (!node) {
172  throw nav2_core::RouteException("Route tracker node expired");
173  }
174 
175  nav2::Rate r(node, tracker_update_rate_);
176  while (rclcpp::ok()) {
177  bool status_change = false, completed = false;
178 
179  // Check if OK to keep processing
180  if (action_server_->is_cancel_requested()) {
181  return TrackerResult::INTERRUPTED;
182  } else if (action_server_->is_preempt_requested()) {
183  return TrackerResult::INTERRUPTED;
184  }
185 
186  // Update the tracking state
187  geometry_msgs::msg::PoseStamped robot_pose = getRobotPose();
188  if (nodeAchieved(robot_pose, state, route)) {
189  status_change = true;
190  state.within_radius = false;
191  state.last_node = state.next_node;
192  if (++state.route_edges_idx < static_cast<int>(route.edges.size())) {
193  state.current_edge = route.edges[state.route_edges_idx];
194  state.next_node = state.current_edge->end;
195  } else { // At achieved the last node in the route
196  state.current_edge = nullptr;
197  state.next_node = nullptr;
198  completed = true;
199  }
200  }
201 
202  // Process any operations necessary
203  OperationsResult ops_result =
204  operations_manager_->process(status_change, state, route, robot_pose, rerouting_info);
205 
206  if (completed) {
207  RCLCPP_INFO(logger_, "Routing to goal completed!");
208  // Publishing last feedback
209  publishFeedback(false, 0, state.last_node->nodeid, 0, ops_result.operations_triggered);
210  return TrackerResult::COMPLETED;
211  }
212 
213  if ((status_change || !ops_result.operations_triggered.empty()) && state.current_edge) {
215  false, // No rerouting occurred
216  state.next_node->nodeid, state.last_node->nodeid,
217  state.current_edge->edgeid, ops_result.operations_triggered);
218  }
219 
220  if (ops_result.reroute) {
221  if (!aggregate_blocked_ids_) {
222  rerouting_info.blocked_ids = ops_result.blocked_ids;
223  } else {
224  rerouting_info.blocked_ids.insert(
225  rerouting_info.blocked_ids.end(),
226  ops_result.blocked_ids.begin(), ops_result.blocked_ids.end());
227  }
228 
229  if (state.last_node) {
230  rerouting_info.rerouting_start_id = state.last_node->nodeid;
231  rerouting_info.rerouting_start_pose = robot_pose;
232  } else {
233  rerouting_info.rerouting_start_id = std::numeric_limits<unsigned int>::max();
234  rerouting_info.rerouting_start_pose = geometry_msgs::msg::PoseStamped();
235  }
236 
237  // Update so during rerouting we can check if we are continuing on the same edge
238  rerouting_info.curr_edge = state.current_edge;
239  RCLCPP_INFO(logger_, "Rerouting requested by route tracking operations!");
240  return TrackerResult::INTERRUPTED;
241  }
242 
243  r.sleep();
244  }
245 
246  return TrackerResult::EXITED;
247 }
248 
249 } // namespace nav2_route
A sim-time-aware rate for Nav2 loops.
Definition: rate.hpp:61
void configure(nav2::LifecycleNode::SharedPtr node, nav2::TransformBuffer::SharedPtr tf_buffer, std::shared_ptr< nav2_costmap_2d::CostmapSubscriber > costmap_subscriber, typename ActionServerTrack::SharedPtr action_server, const std::string &route_frame, const std::string &base_frame)
Configure route tracker.
bool nodeAchieved(const geometry_msgs::msg::PoseStamped &pose, RouteTrackingState &state, const Route &route)
Determine if a node is to be considered achieved at the current position.
TrackerResult trackRoute(const Route &route, const nav_msgs::msg::Path &path, ReroutingState &rerouting_info)
Main function to track route, manage state, and trigger operations.
void publishFeedback(const bool rereouted, const unsigned int next_node_id, const unsigned int last_node_id, const unsigned int edge_id, const std::vector< std::string > &operations)
A utility to publish feedback for the action on important changes.
geometry_msgs::msg::PoseStamped getRobotPose()
Get the current robot's base_frame pose in route_frame.
bool isStartOrEndNode(RouteTrackingState &state, const Route &route)
Determine if a node is the start or last node in the route.
An object to store the nodes in the graph file.
Definition: types.hpp:183
Result information from the operations manager.
Definition: types.hpp:123
State shared to objects to communicate important rerouting data to avoid rerouting over blocked edges...
Definition: types.hpp:264
Current state management of route tracking class.
Definition: types.hpp:248
An ordered set of nodes and edges corresponding to the planned route.
Definition: types.hpp:211