Files
yiliao2026/src/navigation/obstacle_nav2/src/obstacle_array_layer.cpp
2026-08-05 22:17:53 +08:00

417 lines
14 KiB
C++
Executable File

#include "obstacle_nav2/obstacle_array_layer.hpp"
#include <algorithm>
#include <chrono>
#include <cmath>
#include <optional>
#include <string>
#include <vector>
#include "geometry_msgs/msg/point_stamped.hpp"
#include "pluginlib/class_list_macros.hpp"
#include "tf2_geometry_msgs/tf2_geometry_msgs.hpp"
namespace obstacle_nav2
{
ObstacleArrayLayer::ObstacleArrayLayer() = default;
ObstacleArrayLayer::~ObstacleArrayLayer()
{
callback_stop_.store(true, std::memory_order_release);
if (callback_executor_) {
callback_executor_->cancel();
}
if (callback_thread_.joinable()) {
callback_thread_.join();
}
}
void ObstacleArrayLayer::onInitialize()
{
// CRITICAL: Match the internal costmap size to the master costmap.
// CostmapLayer::matchSize() is normally called during updateMap(),
// but obstacles arrive during CONFIGURE phase when subscriptions
// are active. We need the buffer sized now so rasterizeCircle works.
CostmapLayer::onInitialize();
if (layered_costmap_) {
auto * master = layered_costmap_->getCostmap();
resizeMap(
master->getSizeInCellsX(), master->getSizeInCellsY(),
master->getResolution(),
master->getOriginX(), master->getOriginY());
}
// Lock the base-class LifecycleNode weak_ptr
auto node = node_.lock();
if (!node) {
RCLCPP_ERROR(
rclcpp::get_logger("obstacle_array_layer"),
"Failed to lock node in onInitialize");
return;
}
// Parameters (prefixed with layer name per Nav2 convention)
node->declare_parameter(name_ + ".enabled", true);
node->declare_parameter(name_ + ".topic", std::string("/obstacles"));
node->declare_parameter(name_ + ".obstacle_timeout", 0.5);
node->declare_parameter(name_ + ".transform_tolerance", 0.2);
node->declare_parameter(name_ + ".default_obstacle_radius", 0.05);
node->declare_parameter(name_ + ".minimum_obstacle_radius", 0.02);
node->declare_parameter(name_ + ".maximum_obstacle_radius", 0.50);
node->declare_parameter(name_ + ".extra_inflation", 0.02);
node->declare_parameter(name_ + ".retain_previous_on_empty_snapshot", true);
node->get_parameter(name_ + ".enabled", enabled_);
node->get_parameter(name_ + ".topic", topic_);
node->get_parameter(name_ + ".obstacle_timeout", obstacle_timeout_);
node->get_parameter(name_ + ".transform_tolerance", transform_tolerance_);
node->get_parameter(name_ + ".default_obstacle_radius", default_obstacle_radius_);
node->get_parameter(name_ + ".minimum_obstacle_radius", minimum_obstacle_radius_);
node->get_parameter(name_ + ".maximum_obstacle_radius", maximum_obstacle_radius_);
node->get_parameter(name_ + ".extra_inflation", extra_inflation_);
node->get_parameter(
name_ + ".retain_previous_on_empty_snapshot", retain_previous_on_empty_snapshot_);
global_frame_ = layered_costmap_->getGlobalFrameID();
// Costmap plugins are configured after the parent node has entered its
// executor. Use an explicit callback group so this late-created subscription
// is guaranteed to be serviced on Humble.
callback_group_ = node->create_callback_group(
rclcpp::CallbackGroupType::MutuallyExclusive, false);
rclcpp::SubscriptionOptions subscription_options;
subscription_options.callback_group = callback_group_;
obstacle_sub_ = node->create_subscription<obstacle_scanner::msg::ObstacleArray>(
topic_, rclcpp::QoS(10).reliable(),
std::bind(&ObstacleArrayLayer::obstacleCallback, this, std::placeholders::_1),
subscription_options);
callback_executor_ = std::make_unique<rclcpp::executors::SingleThreadedExecutor>();
callback_executor_->add_callback_group(
callback_group_, node->get_node_base_interface());
callback_stop_.store(false, std::memory_order_release);
callback_thread_ = std::thread(
[this]() {
while (!callback_stop_.load(std::memory_order_acquire)) {
callback_executor_->spin_once(std::chrono::milliseconds(100));
}
});
current_ = true;
has_received_obstacles_ = false;
last_obstacle_time_ = node->now();
}
void ObstacleArrayLayer::activate() {}
void ObstacleArrayLayer::deactivate() {}
void ObstacleArrayLayer::reset()
{
std::lock_guard<std::mutex> lock(data_mutex_);
pending_clear_bounds_ = mergeBounds(pending_clear_bounds_, current_bounds_);
if (pending_clear_bounds_.valid) {
++bounds_generation_;
}
resetMap(0, 0, getSizeInCellsX(), getSizeInCellsY());
current_bounds_ = SnapshotBounds();
has_received_obstacles_ = false;
}
bool ObstacleArrayLayer::isClearable()
{
return true;
}
void ObstacleArrayLayer::updateBounds(
double robot_x, double robot_y, double /*robot_yaw*/,
double * min_x, double * min_y,
double * max_x, double * max_y)
{
auto node = node_.lock();
if (!node) {return;}
bool enabled;
node->get_parameter_or<bool>(name_ + ".enabled", enabled, true);
if (!enabled) {return;}
std::lock_guard<std::mutex> lock(data_mutex_);
if (layered_costmap_->isRolling()) {
updateOrigin(
robot_x - getSizeInMetersX() / 2.0,
robot_y - getSizeInMetersY() / 2.0);
}
clearExpiredObstacles(node->now());
const auto bounds = mergeBounds(current_bounds_, pending_clear_bounds_);
if (bounds.valid) {
touch(bounds.min_x, bounds.min_y, min_x, min_y, max_x, max_y);
touch(bounds.max_x, bounds.max_y, min_x, min_y, max_x, max_y);
}
bounds_generation_used_ = bounds_generation_;
}
void ObstacleArrayLayer::updateCosts(
nav2_costmap_2d::Costmap2D & master_grid,
int min_i, int min_j, int max_i, int max_j)
{
auto node = node_.lock();
if (!node) {return;}
bool enabled;
node->get_parameter_or<bool>(name_ + ".enabled", enabled, true);
if (!enabled) {return;}
std::lock_guard<std::mutex> lock(data_mutex_);
updateWithMax(master_grid, min_i, min_j, max_i, max_j);
if (bounds_generation_used_ == bounds_generation_) {
pending_clear_bounds_ = SnapshotBounds();
}
}
// ============================================================================
// Static helpers
// ============================================================================
void ObstacleArrayLayer::rasterizeCircle(
nav2_costmap_2d::Costmap2D & grid,
double cx, double cy, double radius,
double resolution, double origin_x, double origin_y)
{
if (radius < 0.0) {
return;
}
// Handle zero radius: mark only the cell containing the center
if (radius <= 0.0) {
unsigned int cx_cell, cy_cell;
if (grid.worldToMap(cx, cy, cx_cell, cy_cell)) {
grid.setCost(cx_cell, cy_cell, nav2_costmap_2d::LETHAL_OBSTACLE);
}
return;
}
double radius_sq = radius * radius;
unsigned int size_x = grid.getSizeInCellsX();
unsigned int size_y = grid.getSizeInCellsY();
// Bounding box in cell indices
int ix_min = static_cast<int>(std::floor((cx - radius - origin_x) / resolution));
int iy_min = static_cast<int>(std::floor((cy - radius - origin_y) / resolution));
int ix_max = static_cast<int>(std::ceil((cx + radius - origin_x) / resolution));
int iy_max = static_cast<int>(std::ceil((cy + radius - origin_y) / resolution));
if (ix_min < 0) {ix_min = 0;}
if (iy_min < 0) {iy_min = 0;}
if (ix_max >= static_cast<int>(size_x)) {ix_max = static_cast<int>(size_x) - 1;}
if (iy_max >= static_cast<int>(size_y)) {iy_max = static_cast<int>(size_y) - 1;}
for (int j = iy_min; j <= iy_max; ++j) {
for (int i = ix_min; i <= ix_max; ++i) {
double wx, wy;
grid.mapToWorld(
static_cast<unsigned int>(i),
static_cast<unsigned int>(j), wx, wy);
double dx = wx - cx;
double dy = wy - cy;
if (dx * dx + dy * dy <= radius_sq) {
grid.setCost(
static_cast<unsigned int>(i),
static_cast<unsigned int>(j),
nav2_costmap_2d::LETHAL_OBSTACLE);
}
}
}
}
double ObstacleArrayLayer::clampRadius(
double radius, double min_r, double max_r, double default_r)
{
if (!std::isfinite(radius) || radius <= 0.0) {
return default_r;
}
if (radius < min_r) {
return min_r;
}
if (radius > max_r) {
return max_r;
}
return radius;
}
std::vector<ObstacleArrayLayer::CircleObstacle> ObstacleArrayLayer::transformSnapshot(
const obstacle_scanner::msg::ObstacleArray & snapshot,
const std::string & global_frame,
tf2_ros::Buffer & tf_buffer,
const rclcpp::Duration & timeout)
{
std::vector<CircleObstacle> transformed;
transformed.reserve(snapshot.obstacles.size());
if (snapshot.obstacles.empty()) {
return transformed;
}
const rclcpp::Time stamp(snapshot.header.stamp);
const auto transform = timeout.nanoseconds() > 0 ?
tf_buffer.lookupTransform(global_frame, snapshot.header.frame_id, stamp, timeout) :
tf_buffer.lookupTransform(global_frame, snapshot.header.frame_id, stamp);
for (const auto & obstacle : snapshot.obstacles) {
geometry_msgs::msg::PointStamped input;
geometry_msgs::msg::PointStamped output;
input.header = snapshot.header;
input.point.x = obstacle.center_x;
input.point.y = obstacle.center_y;
input.point.z = 0.0;
tf2::doTransform(input, output, transform);
transformed.push_back({output.point.x, output.point.y, obstacle.radius});
}
return transformed;
}
ObstacleArrayLayer::SnapshotBounds ObstacleArrayLayer::applySnapshot(
nav2_costmap_2d::Costmap2D & grid,
const std::vector<CircleObstacle> & obstacles,
double resolution, double origin_x, double origin_y)
{
grid.resetMap(0, 0, grid.getSizeInCellsX(), grid.getSizeInCellsY());
SnapshotBounds bounds;
for (const auto & obstacle : obstacles) {
rasterizeCircle(
grid, obstacle.center_x, obstacle.center_y, obstacle.radius,
resolution, origin_x, origin_y);
if (!bounds.valid) {
bounds.valid = true;
bounds.min_x = obstacle.center_x - obstacle.radius;
bounds.min_y = obstacle.center_y - obstacle.radius;
bounds.max_x = obstacle.center_x + obstacle.radius;
bounds.max_y = obstacle.center_y + obstacle.radius;
} else {
bounds.min_x = std::min(bounds.min_x, obstacle.center_x - obstacle.radius);
bounds.min_y = std::min(bounds.min_y, obstacle.center_y - obstacle.radius);
bounds.max_x = std::max(bounds.max_x, obstacle.center_x + obstacle.radius);
bounds.max_y = std::max(bounds.max_y, obstacle.center_y + obstacle.radius);
}
}
return bounds;
}
std::optional<ObstacleArrayLayer::SnapshotBounds> ObstacleArrayLayer::applySnapshotIfNotEmpty(
nav2_costmap_2d::Costmap2D & grid,
const std::vector<CircleObstacle> & obstacles,
double resolution, double origin_x, double origin_y)
{
if (obstacles.empty()) {
return std::nullopt;
}
return applySnapshot(grid, obstacles, resolution, origin_x, origin_y);
}
ObstacleArrayLayer::SnapshotBounds ObstacleArrayLayer::mergeBounds(
const SnapshotBounds & first, const SnapshotBounds & second)
{
if (!first.valid) {
return second;
}
if (!second.valid) {
return first;
}
SnapshotBounds merged;
merged.valid = true;
merged.min_x = std::min(first.min_x, second.min_x);
merged.min_y = std::min(first.min_y, second.min_y);
merged.max_x = std::max(first.max_x, second.max_x);
merged.max_y = std::max(first.max_y, second.max_y);
return merged;
}
void ObstacleArrayLayer::clearExpiredObstacles(const rclcpp::Time & now)
{
if (!has_received_obstacles_) {
return;
}
if ((now - last_obstacle_time_).seconds() <= obstacle_timeout_) {
return;
}
pending_clear_bounds_ = mergeBounds(pending_clear_bounds_, current_bounds_);
if (pending_clear_bounds_.valid) {
++bounds_generation_;
}
resetMap(0, 0, getSizeInCellsX(), getSizeInCellsY());
current_bounds_ = SnapshotBounds();
has_received_obstacles_ = false;
}
// ============================================================================
// Private
// ============================================================================
void ObstacleArrayLayer::obstacleCallback(
const obstacle_scanner::msg::ObstacleArray::SharedPtr msg)
{
auto node = node_.lock();
if (!node) {return;}
bool enabled;
node->get_parameter_or<bool>(name_ + ".enabled", enabled, true);
if (!enabled) {return;}
std::vector<CircleObstacle> transformed;
try {
transformed = transformSnapshot(
*msg, global_frame_, *tf_,
rclcpp::Duration::from_seconds(transform_tolerance_));
} catch (const tf2::TransformException & e) {
RCLCPP_WARN_THROTTLE(
node->get_logger(), *node->get_clock(), 5000,
"ObstacleArrayLayer: TF failed at message stamp: %s", e.what());
return;
}
std::vector<CircleObstacle> valid_obstacles;
valid_obstacles.reserve(transformed.size());
const double origin_x = getOriginX();
const double origin_y = getOriginY();
const double map_x_max = origin_x + getSizeInMetersX();
const double map_y_max = origin_y + getSizeInMetersY();
for (const auto & obs : transformed) {
if (!std::isfinite(obs.center_x) || !std::isfinite(obs.center_y)) {
continue;
}
double radius = clampRadius(
obs.radius, minimum_obstacle_radius_, maximum_obstacle_radius_,
default_obstacle_radius_);
double effective_r = radius + extra_inflation_;
if (obs.center_x + effective_r < origin_x ||
obs.center_x - effective_r > map_x_max ||
obs.center_y + effective_r < origin_y ||
obs.center_y - effective_r > map_y_max)
{
continue;
}
valid_obstacles.push_back({obs.center_x, obs.center_y, effective_r});
}
if (valid_obstacles.empty() && retain_previous_on_empty_snapshot_) {
return;
}
std::lock_guard<std::mutex> lock(data_mutex_);
const SnapshotBounds previous_bounds = current_bounds_;
current_bounds_ = applySnapshot(
*this, valid_obstacles, getResolution(), origin_x, origin_y);
pending_clear_bounds_ = mergeBounds(pending_clear_bounds_, previous_bounds);
if (previous_bounds.valid || current_bounds_.valid) {
++bounds_generation_;
}
has_received_obstacles_ = current_bounds_.valid;
last_obstacle_time_ = node->now();
}
} // namespace obstacle_nav2
PLUGINLIB_EXPORT_CLASS(
obstacle_nav2::ObstacleArrayLayer,
nav2_costmap_2d::Layer)