-
Notifications
You must be signed in to change notification settings - Fork 34
Add timer-based propagation functionality to AMCL #492
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
8479fd0
8086f76
1571ebe
9994687
4f7b10c
ce8b3f4
3ff9266
d13e2fc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -183,6 +183,16 @@ AmclNode::AmclNode(const rclcpp::NodeOptions& options) : BaseAMCLNode{"amcl", "" | |
| "and ignore subsequent ones."; | ||
| declare_parameter("first_map_only", false, descriptor); | ||
| } | ||
|
|
||
| { | ||
| auto descriptor = rcl_interfaces::msg::ParameterDescriptor(); | ||
| descriptor.description = "Frequency in Hz for increased propagation rate. Set to 0 to disable."; | ||
| descriptor.floating_point_range.resize(1); | ||
| descriptor.floating_point_range[0].from_value = 0.0; | ||
| descriptor.floating_point_range[0].to_value = 100.0; | ||
| descriptor.floating_point_range[0].step = 0.0; | ||
| declare_parameter("propagation_rate", rclcpp::ParameterValue(0.0), descriptor); | ||
| } | ||
| } | ||
|
|
||
| AmclNode::~AmclNode() { | ||
|
|
@@ -251,6 +261,19 @@ void AmclNode::do_activate(const rclcpp_lifecycle::State&) { | |
| std::placeholders::_3), | ||
| common_service_qos, common_callback_group_); | ||
| RCLCPP_INFO(get_logger(), "Created request_nomotion_update service"); | ||
|
|
||
| // Setup increased propagation timer if enabled | ||
| { | ||
| const double propagation_freq = get_parameter("propagation_rate").as_double(); | ||
| if (propagation_freq > 0.0) { | ||
| auto period = std::chrono::duration<double>(1.0 / propagation_freq); | ||
| propagation_timer_ = | ||
| create_wall_timer(period, std::bind(&AmclNode::propagation_timer_callback, this), common_callback_group_); | ||
| RCLCPP_INFO(get_logger(), "Created propagation timer at %.1f Hz", propagation_freq); | ||
| // Initialize odometry motion buffer | ||
| odometry_motion_buffer_.clear(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| void AmclNode::do_deactivate(const rclcpp_lifecycle::State&) { | ||
|
|
@@ -259,6 +282,7 @@ void AmclNode::do_deactivate(const rclcpp_lifecycle::State&) { | |
| laser_scan_filter_.reset(); | ||
| laser_scan_sub_.reset(); | ||
| global_localization_server_.reset(); | ||
| propagation_timer_.reset(); | ||
| if (likelihood_field_pub_) { | ||
| likelihood_field_pub_->on_deactivate(); | ||
| } | ||
|
|
@@ -268,6 +292,7 @@ void AmclNode::do_cleanup(const rclcpp_lifecycle::State&) { | |
| particle_filter_.reset(); | ||
| likelihood_field_pub_.reset(); // attaching likelihood_field_pub_ lifespan to particle_filter_ lifespan | ||
| enable_tf_broadcast_ = false; | ||
| odometry_motion_buffer_.clear(); | ||
| } | ||
|
|
||
| auto AmclNode::get_initial_estimate() const -> std::optional<std::pair<Sophus::SE2d, Eigen::Matrix3d>> { | ||
|
|
@@ -466,26 +491,63 @@ void AmclNode::do_periodic_timer_callback() { | |
| } | ||
| } | ||
|
|
||
| void AmclNode::laser_callback(sensor_msgs::msg::LaserScan::ConstSharedPtr laser_scan) { | ||
| if (!particle_filter_) { | ||
| RCLCPP_WARN_THROTTLE( | ||
| get_logger(), *get_clock(), 2000, "Ignoring laser data because the particle filter has not been initialized"); | ||
| return; | ||
| } | ||
|
|
||
| auto AmclNode::get_base_pose_in_odom(const tf2::TimePoint& time) const -> std::optional<Sophus::SE2d> { | ||
| auto base_pose_in_odom = Sophus::SE2d{}; | ||
| try { | ||
| // Use the lookupTransform overload with no timeout since we're not using a dedicated | ||
| // tf thread. The message filter we are using avoids the need for it. | ||
| tf2::convert( | ||
| tf_buffer_ | ||
| ->lookupTransform( | ||
| get_parameter("odom_frame_id").as_string(), get_parameter("base_frame_id").as_string(), | ||
| tf2_ros::fromMsg(laser_scan->header.stamp)) | ||
| get_parameter("odom_frame_id").as_string(), get_parameter("base_frame_id").as_string(), time) | ||
| .transform, | ||
| base_pose_in_odom); | ||
| return base_pose_in_odom; | ||
| } catch (const tf2::TransformException& error) { | ||
| RCLCPP_ERROR(get_logger(), "Could not transform from odom to base: %s", error.what()); | ||
| return std::nullopt; | ||
| } | ||
| } | ||
|
|
||
| void AmclNode::propagation_timer_callback() { | ||
| if (!particle_filter_) { | ||
| RCLCPP_WARN_THROTTLE( | ||
| get_logger(), *get_clock(), 2000, "Ignoring propagation because the particle filter has not been initialized"); | ||
| return; | ||
| } | ||
|
|
||
| const auto base_pose_in_odom = get_base_pose_in_odom(tf2::TimePointZero); | ||
| if (!base_pose_in_odom.has_value()) { | ||
| return; | ||
| } | ||
|
|
||
| const auto now = this->now(); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @fbattocchia
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On a more general note, it's starting to look like time is a relevant quantity we don't often propagate and we probably should. |
||
| const auto time = tf2_ros::fromMsg(now); | ||
| // Queue odometry motion (timestamp, pose) for later processing in laser_callback | ||
| odometry_motion_buffer_.emplace_back(time, base_pose_in_odom.value()); | ||
| } | ||
|
|
||
| void AmclNode::laser_callback(sensor_msgs::msg::LaserScan::ConstSharedPtr laser_scan) { | ||
| if (!particle_filter_) { | ||
| RCLCPP_WARN_THROTTLE( | ||
| get_logger(), *get_clock(), 2000, "Ignoring laser data because the particle filter has not been initialized"); | ||
| return; | ||
| } | ||
|
|
||
| // If propagation_rate is enabled, process odometry buffer up to lidar timestamp | ||
| if (get_parameter("propagation_rate").as_double() > 0.0) { | ||
| const auto laser_scan_stamp = tf2_ros::fromMsg(laser_scan->header.stamp); | ||
| while (!odometry_motion_buffer_.empty()) { | ||
| const auto& [odom_time, odom_pose] = odometry_motion_buffer_.front(); | ||
| if (odom_time >= laser_scan_stamp) { | ||
| break; | ||
| } | ||
| particle_filter_->update(odom_pose); | ||
| odometry_motion_buffer_.pop_front(); | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @fbattocchia meta: it's a bit of a bummer that the feature exists outside core Beluga but this is simple enough. How would you go about adding (some) support for this at the motion model level if we had to?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, I could define an interface in the Beluga motion model to accept a sequence of movements (for example, apply_odometry_sequence or update_odometry_buffer_until) and process them all together |
||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit, but if odometry driven prop is off, the queue will be empty anyway.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @glpuga Removing the
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I'm ok with it staying. |
||
|
|
||
| // Get base pose in odom frame at laser scan timestamp | ||
| const auto base_pose_in_odom = get_base_pose_in_odom(tf2_ros::fromMsg(laser_scan->header.stamp)); | ||
| if (!base_pose_in_odom.has_value()) { | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -505,7 +567,7 @@ void AmclNode::laser_callback(sensor_msgs::msg::LaserScan::ConstSharedPtr laser_ | |
|
|
||
| const auto update_start_time = std::chrono::high_resolution_clock::now(); | ||
| const auto new_estimate = particle_filter_->update( | ||
| base_pose_in_odom, // | ||
| base_pose_in_odom.value(), // | ||
| beluga_ros::LaserScan{ | ||
| laser_scan, | ||
| laser_pose_in_base, | ||
|
|
@@ -518,7 +580,7 @@ void AmclNode::laser_callback(sensor_msgs::msg::LaserScan::ConstSharedPtr laser_ | |
|
|
||
| if (new_estimate.has_value()) { | ||
| const auto& [base_pose_in_map, _] = new_estimate.value(); | ||
| last_known_odom_transform_in_map_ = base_pose_in_map * base_pose_in_odom.inverse(); | ||
| last_known_odom_transform_in_map_ = base_pose_in_map * base_pose_in_odom.value().inverse(); | ||
| last_known_estimate_ = new_estimate; | ||
|
|
||
| RCLCPP_INFO( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should also clear this whenever the particle filter is initialized/created.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm clearing the buffer in
do_activate()where the odometry subscription is performedThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but that only happens when the node starts. The filter might be reinitialized multiple times afterwards, whenever the map is updated or when the user sends a pose intialization message. Look for calls to
initialize_from_estimate()in amcl_node.cpp