Nav2 Navigation Stack - rolling  main
ROS 2 Navigation Stack
zone_parameter_filter.cpp
1 // Copyright (c) 2026 Komada (aki1770-del)
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_costmap_2d/costmap_filters/zone_parameter_filter.hpp"
16 
17 #include <algorithm>
18 #include <chrono>
19 #include <memory>
20 #include <optional>
21 #include <set>
22 #include <string>
23 #include <utility>
24 #include <vector>
25 
26 #include "nav2_costmap_2d/costmap_filters/filter_values.hpp"
27 #include "nav2_util/occ_grid_utils.hpp"
28 
29 namespace nav2_costmap_2d
30 {
31 
32 ZoneParameterFilter::ZoneParameterFilter()
33 : filter_info_sub_(nullptr),
34  mask_sub_(nullptr),
35  state_event_pub_(nullptr),
36  filter_mask_(nullptr),
37  global_frame_("")
38 {
39 }
40 
41 void ZoneParameterFilter::initializeFilter(
42  const std::string & filter_info_topic)
43 {
44  std::lock_guard<CostmapFilter::mutex_t> guard(*getMutex());
45 
46  auto node = node_.lock();
47  if (!node) {
48  throw std::runtime_error{"Failed to lock node"};
49  }
50 
51  global_frame_ = layered_costmap_->getGlobalFrameID();
52  state_event_topic_ =
53  node->declare_or_get_parameter<std::string>(
54  name_ + "." + "state_event_topic", std::string("zone_filter_state"));
55  filter_info_topic_ = joinWithParentNamespace(filter_info_topic);
56  RCLCPP_INFO(
57  logger_,
58  "ZoneParameterFilter: Subscribing to \"%s\" topic for filter info...",
59  filter_info_topic_.c_str());
60 
61  filter_info_sub_ = node->create_subscription<nav2_msgs::msg::CostmapFilterInfo>(
62  filter_info_topic_,
63  std::bind(&ZoneParameterFilter::filterInfoCallback, this, std::placeholders::_1),
65 
66  state_event_pub_ =
67  node->create_publisher<std_msgs::msg::UInt8>(joinWithParentNamespace(state_event_topic_));
68  state_event_pub_->on_activate();
69 
70  loadStateConfig();
71 }
72 
73 void ZoneParameterFilter::filterInfoCallback(
74  const nav2_msgs::msg::CostmapFilterInfo::ConstSharedPtr & msg)
75 {
76  std::lock_guard<CostmapFilter::mutex_t> guard(*getMutex());
77 
78  auto node = node_.lock();
79  if (!node) {
80  throw std::runtime_error{"Failed to lock node"};
81  }
82 
83  if (!mask_sub_) {
84  RCLCPP_INFO(
85  logger_,
86  "ZoneParameterFilter: Received filter info from %s topic.", filter_info_topic_.c_str());
87  } else {
88  RCLCPP_WARN(
89  logger_,
90  "ZoneParameterFilter: New costmap filter info arrived from %s topic. "
91  "Updating old filter info.",
92  filter_info_topic_.c_str());
93  mask_sub_.reset();
94  }
95 
96  if (msg->type != ZONE_PARAMETER_FILTER) {
97  RCLCPP_ERROR(
98  logger_,
99  "ZoneParameterFilter: CostmapFilterInfo type is %i, expected %i (ZONE_PARAMETER_FILTER)",
100  msg->type, ZONE_PARAMETER_FILTER);
101  return;
102  }
103 
104  if (msg->base != BASE_DEFAULT || msg->multiplier != MULTIPLIER_DEFAULT) {
105  RCLCPP_WARN(
106  logger_,
107  "ZoneParameterFilter: base=%f and multiplier=%f are unused by this filter "
108  "(state mapping is config-driven). Expected defaults (%f, %f).",
109  msg->base, msg->multiplier, BASE_DEFAULT, MULTIPLIER_DEFAULT);
110  }
111 
112  filter_info_received_ = true;
113  mask_topic_ = joinWithParentNamespace(msg->filter_mask_topic);
114 
115  RCLCPP_INFO(
116  logger_,
117  "ZoneParameterFilter: Subscribing to \"%s\" topic for filter mask...",
118  mask_topic_.c_str());
119  mask_sub_ = node->create_subscription<nav_msgs::msg::OccupancyGrid>(
120  mask_topic_,
121  std::bind(&ZoneParameterFilter::maskCallback, this, std::placeholders::_1),
123 }
124 
125 void ZoneParameterFilter::maskCallback(
126  const nav_msgs::msg::OccupancyGrid::ConstSharedPtr & msg)
127 {
128  std::lock_guard<CostmapFilter::mutex_t> guard(*getMutex());
129 
130  if (!filter_mask_) {
131  RCLCPP_INFO(
132  logger_,
133  "ZoneParameterFilter: Received filter mask from %s topic.", mask_topic_.c_str());
134  } else {
135  RCLCPP_WARN(
136  logger_,
137  "ZoneParameterFilter: New filter mask arrived from %s topic. Updating old filter mask.",
138  mask_topic_.c_str());
139  filter_mask_.reset();
140  }
141 
142  filter_mask_ = msg;
143 }
144 
145 void ZoneParameterFilter::loadStateConfig()
146 {
147  auto node = node_.lock();
148  if (!node) {
149  throw std::runtime_error{"Failed to lock node"};
150  }
151 
152  // Obtain the node, parameter, and value for state entries
153  auto read_entry =
154  [&](const std::string & prefix) -> std::optional<StateParamEntry> {
155  const std::string target_node =
156  node->declare_or_get_parameter<std::string>(prefix + ".node", std::string(""));
157  const std::string param_name =
158  node->declare_or_get_parameter<std::string>(prefix + ".parameter", std::string(""));
159  if (target_node.empty() || param_name.empty()) {
160  RCLCPP_ERROR(
161  logger_,
162  "ZoneParameterFilter: '%s' must declare non-empty 'node' and 'parameter'.",
163  prefix.c_str());
164  return std::nullopt;
165  }
166  const std::string value_key = prefix + ".value";
167  if (!node->has_parameter(value_key)) {
168  rcl_interfaces::msg::ParameterDescriptor descriptor;
169  descriptor.dynamic_typing = true;
170  node->declare_parameter(value_key, rclcpp::ParameterValue{}, descriptor);
171  }
172  const rclcpp::Parameter value_param = node->get_parameter(value_key);
173  if (value_param.get_type() == rclcpp::ParameterType::PARAMETER_NOT_SET) {
174  RCLCPP_ERROR(logger_, "ZoneParameterFilter: '%s' is not set.", value_key.c_str());
175  return std::nullopt;
176  }
177  return StateParamEntry{
178  target_node, rclcpp::Parameter(param_name, value_param.get_parameter_value())};
179  };
180 
181  const std::vector<std::string> state_names =
182  node->declare_or_get_parameter<std::vector<std::string>>(
183  name_ + ".states", std::vector<std::string>{});
184 
185  if (state_names.empty()) {
186  RCLCPP_WARN(
187  logger_,
188  "ZoneParameterFilter: 'states' is empty; this filter will only handle "
189  "state 0 (reset). Configure `states: [name_a, ...]` in YAML.");
190  }
191 
192  for (const auto & state_name : state_names) {
193  const std::string state_prefix = name_ + "." + state_name;
194 
195  const int64_t id_i64 =
196  node->declare_or_get_parameter<int64_t>(state_prefix + ".id", 0);
197  if (id_i64 <= 0 || id_i64 > 255) {
198  RCLCPP_ERROR(
199  logger_,
200  "ZoneParameterFilter: state '%s' has id %ld outside the valid range "
201  "[1, 255] (0 is reserved for reset); skipping.",
202  state_name.c_str(), id_i64);
203  continue;
204  }
205  const uint8_t state_id = static_cast<uint8_t>(id_i64);
206 
207  const std::vector<std::string> setpoint_names =
208  node->declare_or_get_parameter<std::vector<std::string>>(
209  state_prefix + ".setpoints", std::vector<std::string>{});
210 
211  std::vector<StateParamEntry> params_for_state;
212  for (const auto & setpoint_name : setpoint_names) {
213  if (auto entry = read_entry(state_prefix + "." + setpoint_name)) {
214  params_for_state.push_back(std::move(*entry));
215  }
216  }
217 
218  if (params_for_state.empty()) {
219  RCLCPP_WARN(
220  logger_,
221  "ZoneParameterFilter: state '%s' (id %u) declares no valid setpoints.",
222  state_name.c_str(), state_id);
223  }
224 
225  state_param_map_[state_id] = std::move(params_for_state);
226  RCLCPP_INFO(
227  logger_,
228  "ZoneParameterFilter: state '%s' (id %u) -> %zu setpoint(s).",
229  state_name.c_str(), state_id, state_param_map_[state_id].size());
230  }
231 
232  // `nominal_defaults`: the baseline values restored on the state-0 reset.
233  const std::vector<std::string> nominal_names =
234  node->declare_or_get_parameter<std::vector<std::string>>(
235  name_ + ".nominal_defaults", std::vector<std::string>{});
236  for (const auto & nominal_name : nominal_names) {
237  if (auto entry = read_entry(name_ + ".nominal_defaults." + nominal_name)) {
238  nominal_defaults_[entry->target_node].push_back(entry->param);
239  }
240  }
241  RCLCPP_INFO(
242  logger_,
243  "ZoneParameterFilter: %zu nominal default(s) loaded for state-0 reset.",
244  nominal_names.size());
245 
246  // Warn on a state override with no matching nominal_default: the state-0
247  // reset would not be able to restore that parameter.
248  auto has_nominal = [this](const StateParamEntry & e) -> bool {
249  const auto node_it = nominal_defaults_.find(e.target_node);
250  if (node_it == nominal_defaults_.end()) {
251  return false;
252  }
253  for (const auto & nominal : node_it->second) {
254  if (nominal.get_name() == e.param.get_name()) {
255  return true;
256  }
257  }
258  return false;
259  };
260  for (const auto & [state_id, entries] : state_param_map_) {
261  for (const auto & entry : entries) {
262  if (!has_nominal(entry)) {
263  RCLCPP_WARN(
264  logger_,
265  "ZoneParameterFilter: state id %u sets '%s' on '%s' but no matching "
266  "nominal_defaults entry exists; state-0 reset will NOT restore it.",
267  state_id, entry.param.get_name().c_str(), entry.target_node.c_str());
268  }
269  }
270  }
271 
272  // Per-node client construction
273  std::set<std::string> all_target_nodes;
274  for (const auto & [_state_id, entries] : state_param_map_) {
275  for (const auto & e : entries) {
276  all_target_nodes.insert(e.target_node);
277  }
278  }
279  for (const auto & [target_node, _params] : nominal_defaults_) {
280  all_target_nodes.insert(target_node);
281  }
282  for (const auto & target_node : all_target_nodes) {
283  param_clients_.emplace(
284  target_node,
285  std::make_shared<rclcpp::AsyncParametersClient>(
286  node->get_node_base_interface(),
287  node->get_node_topics_interface(),
288  node->get_node_graph_interface(),
289  node->get_node_services_interface(),
290  target_node));
291  }
292  RCLCPP_INFO(
293  logger_,
294  "ZoneParameterFilter: %zu AsyncParametersClient(s) built at init.",
295  param_clients_.size());
296 }
297 
298 void ZoneParameterFilter::process(
299  nav2_costmap_2d::Costmap2D & /*master_grid*/,
300  int /*min_i*/, int /*min_j*/, int /*max_i*/, int /*max_j*/,
301  const geometry_msgs::msg::Pose & pose)
302 {
303  std::lock_guard<CostmapFilter::mutex_t> guard(*getMutex());
304 
305  checkPendingParameterUpdates();
306 
307  if (!filter_mask_) {
308  RCLCPP_WARN_THROTTLE(
309  logger_, *(clock_), 2000,
310  "ZoneParameterFilter: Filter mask was not received");
311  return;
312  }
313 
314  geometry_msgs::msg::Pose mask_pose;
315  if (!transformPose(global_frame_, pose, filter_mask_->header.frame_id, mask_pose)) {
316  return;
317  }
318 
319  unsigned int mask_robot_i, mask_robot_j;
320  if (!nav2_util::worldToMap(
321  filter_mask_, mask_pose.position.x, mask_pose.position.y,
322  mask_robot_i, mask_robot_j))
323  {
324  if (state_initialized_ && current_state_ != 0) {
325  RCLCPP_WARN(
326  logger_,
327  "ZoneParameterFilter: Robot outside filter mask; resetting to nominal defaults.");
328  applyState(0);
329  current_state_ = 0;
330  }
331  return;
332  }
333 
334  const int8_t mask_data = getMaskData(filter_mask_, mask_robot_i, mask_robot_j);
335  if (mask_data < 0) {
336  // mask_data < 0 is OCC_GRID_UNKNOWN; don't change state on an unknown cell.
337  RCLCPP_WARN_THROTTLE(
338  logger_, *(clock_), 2000,
339  "ZoneParameterFilter: Filter mask cell [%u, %u] is unknown; not changing state.",
340  mask_robot_i, mask_robot_j);
341  return;
342  }
343 
344  const uint8_t new_state = static_cast<uint8_t>(mask_data);
345 
346  if (state_initialized_ && new_state == current_state_) {
347  return; // No change.
348  }
349 
350  applyState(new_state);
351 
352  if (state_event_pub_) {
353  auto event_msg = std::make_unique<std_msgs::msg::UInt8>();
354  event_msg->data = new_state;
355  state_event_pub_->publish(std::move(event_msg));
356  }
357 
358  current_state_ = new_state;
359  state_initialized_ = true;
360 }
361 
362 void ZoneParameterFilter::applyState(uint8_t new_state)
363 {
364  if (new_state == 0) {
365  resetToNominal();
366  RCLCPP_INFO(logger_, "ZoneParameterFilter: Entered state 0 (reset to nominal).");
367  return;
368  }
369 
370  auto it = state_param_map_.find(new_state);
371  if (it == state_param_map_.end()) {
372  throw std::runtime_error(
373  std::string("ZoneParameterFilter: unknown state ") +
374  std::to_string(new_state) +
375  " encountered; declare a state with id " +
376  std::to_string(new_state) + " under the filter's `states` list.");
377  }
378 
379  std::set<std::pair<std::string, std::string>> m_keys;
380  for (const auto & entry : it->second) {
381  m_keys.emplace(entry.target_node, entry.param.get_name());
382  }
383 
384  // Reset params touched by the previous state N but not the destination M
385  // back to nominal_defaults before applying M's overrides. This preserves
386  // the invariant that all params equal state-0 defaults except those
387  // specifically set in the active state.
388  // Reset params touched by the previous state N but not the destination M
389  // back to nominal_defaults before applying M's overrides.
390  std::map<std::string, std::vector<rclcpp::Parameter>> reset_per_node;
391  if (state_initialized_ && current_state_ != 0) {
392  auto prev_it = state_param_map_.find(current_state_);
393  if (prev_it != state_param_map_.end()) {
394  for (const auto & entry : prev_it->second) {
395  const auto key = std::make_pair(entry.target_node, entry.param.get_name());
396  if (m_keys.count(key) > 0) {
397  continue; // M will set this param; reset is wasted work.
398  }
399  const auto node_it = nominal_defaults_.find(entry.target_node);
400  if (node_it == nominal_defaults_.end()) {
401  // No nominal_defaults were declared for this node, so this param has
402  // nothing to reset to; it keeps state N's value. Warned at config-load.
403  continue;
404  }
405  for (const auto & nominal : node_it->second) {
406  if (nominal.get_name() == entry.param.get_name()) {
407  reset_per_node[entry.target_node].push_back(nominal);
408  break;
409  }
410  }
411  }
412  } else {
413  RCLCPP_ERROR(
414  logger_,
415  "ZoneParameterFilter: current state %u is not in state_param_map_ "
416  "(should have been recorded when the state was applied).",
417  current_state_);
418  }
419  }
420 
421  // Batch per target node (one set_parameters call per node).
422  std::map<std::string, std::vector<rclcpp::Parameter>> per_node_params;
423  for (const auto & entry : it->second) {
424  per_node_params[entry.target_node].push_back(entry.param);
425  }
426 
427  // Submit the N-only resets before M's overrides.
428  size_t reset_count = 0;
429  for (const auto & [target_node, params] : reset_per_node) {
430  issueAsyncSetParameters(target_node, params);
431  reset_count += params.size();
432  }
433 
434  for (const auto & [target_node, params] : per_node_params) {
435  issueAsyncSetParameters(target_node, params);
436  }
437 
438  RCLCPP_INFO(
439  logger_,
440  "ZoneParameterFilter: Entered state %u (reset %zu N-only parameter(s); "
441  "applied %zu parameter(s) across %zu node(s)).",
442  new_state, reset_count, it->second.size(), per_node_params.size());
443 }
444 
445 void ZoneParameterFilter::resetToNominal()
446 {
447  for (const auto & [target_node, params] : nominal_defaults_) {
448  issueAsyncSetParameters(target_node, params);
449  }
450 }
451 
452 void ZoneParameterFilter::issueAsyncSetParameters(
453  const std::string & target_node,
454  const std::vector<rclcpp::Parameter> & params)
455 {
456  auto client_it = param_clients_.find(target_node);
457  if (client_it == param_clients_.end()) {
458  RCLCPP_ERROR(
459  logger_,
460  "ZoneParameterFilter: no client for target_node '%s' "
461  "(should have been built at config-load).",
462  target_node.c_str());
463  return;
464  }
465 
466  pending_futures_.push_back(client_it->second->set_parameters(params));
467 }
468 
469 void ZoneParameterFilter::checkPendingParameterUpdates()
470 {
471  // A silently-swallowed set failure would leave the robot on the value the
472  // safety zone tried to change (worse than surfacing it), so failures throw
473  // rather than get logged-and-ignored.
474  // wait_for(0s) polls without blocking the costmap update loop
475  auto it = pending_futures_.begin();
476  while (it != pending_futures_.end()) {
477  if (it->wait_for(std::chrono::seconds(0)) != std::future_status::ready) {
478  ++it;
479  continue;
480  }
481 
482  // Copy the shared state out before erasing: the copy keeps the value that
483  // get() returns a reference to alive for this iteration, and erasing first
484  // means a service-side exception rethrown by get() surfaces exactly once.
485  const auto ready_future = *it;
486  it = pending_futures_.erase(it);
487 
488  // get() returns a const reference; bind it rather than copy the vector.
489  const auto & results = ready_future.get();
490  for (const auto & r : results) {
491  if (!r.successful) {
492  throw std::runtime_error(
493  "ZoneParameterFilter: set_parameters failed: " + r.reason);
494  }
495  }
496  }
497 }
498 
499 void ZoneParameterFilter::resetFilter()
500 {
501  std::lock_guard<CostmapFilter::mutex_t> guard(*getMutex());
502 
503  filter_info_sub_.reset();
504  mask_sub_.reset();
505  if (state_event_pub_) {
506  state_event_pub_->on_deactivate();
507  state_event_pub_.reset();
508  }
509 
510  filter_mask_.reset();
511  filter_info_received_ = false;
512  state_initialized_ = false;
513  current_state_ = 0;
514 }
515 
516 bool ZoneParameterFilter::isActive()
517 {
518  std::lock_guard<CostmapFilter::mutex_t> guard(*getMutex());
519  return filter_mask_ != nullptr;
520 }
521 
522 } // namespace nav2_costmap_2d
523 
524 #include "pluginlib/class_list_macros.hpp"
A QoS profile for latched, reliable topics with a history of 10 messages.
A 2D costmap provides a mapping between points in the world and their associated "costs".
Definition: costmap_2d.hpp:69
Abstract class for layered costmap plugin implementations.
Definition: layer.hpp:60
Costmap filter that applies a configured set of ROS parameters based on the mask value at the robot's...