From cba873a899e6057c48e4ee07c49c4327b24c7a7d Mon Sep 17 00:00:00 2001 From: Paul Gesel Date: Fri, 19 Jan 2024 07:00:42 -0700 Subject: [PATCH] Acceleration Limited Smoothing Plugin for Servo (#2651) Signed-off-by: Paul Gesel --- moveit_core/CMakeLists.txt | 7 +- moveit_core/filter_plugin_acceleration.xml | 8 + .../online_signal_smoothing/CMakeLists.txt | 38 +- moveit_core/online_signal_smoothing/README.md | 12 + .../acceleration_filter.h | 162 ++++++++ .../butterworth_filter.h | 3 +- .../res/acceleration_limiting_diagram.png | Bin 0 -> 68588 bytes .../src/acceleration_filter.cpp | 349 ++++++++++++++++++ .../src/acceleration_filter_parameters.yaml | 16 + .../test/test_acceleration_filter.cpp | 207 +++++++++++ moveit_core/package.xml | 9 +- .../config/panda_simulated_config.yaml | 2 +- .../launch/demo_joint_jog.launch.py | 2 +- .../moveit_servo/launch/demo_pose.launch.py | 2 +- .../launch/demo_ros_api.launch.py | 15 +- .../moveit_servo/launch/demo_twist.launch.py | 2 +- moveit_ros/moveit_servo/src/servo.cpp | 24 +- 17 files changed, 826 insertions(+), 32 deletions(-) create mode 100644 moveit_core/filter_plugin_acceleration.xml create mode 100644 moveit_core/online_signal_smoothing/README.md create mode 100644 moveit_core/online_signal_smoothing/include/moveit/online_signal_smoothing/acceleration_filter.h create mode 100644 moveit_core/online_signal_smoothing/res/acceleration_limiting_diagram.png create mode 100644 moveit_core/online_signal_smoothing/src/acceleration_filter.cpp create mode 100644 moveit_core/online_signal_smoothing/src/acceleration_filter_parameters.yaml create mode 100644 moveit_core/online_signal_smoothing/test/test_acceleration_filter.cpp diff --git a/moveit_core/CMakeLists.txt b/moveit_core/CMakeLists.txt index 107ce7fa6f..499732ea7c 100644 --- a/moveit_core/CMakeLists.txt +++ b/moveit_core/CMakeLists.txt @@ -20,6 +20,7 @@ find_package(kdl_parser REQUIRED) find_package(moveit_msgs REQUIRED) find_package(OCTOMAP REQUIRED) find_package(octomap_msgs REQUIRED) +find_package(osqp REQUIRED) find_package(pluginlib REQUIRED) find_package(random_numbers REQUIRED) find_package(rclcpp REQUIRED) @@ -69,8 +70,10 @@ add_subdirectory(version) install( TARGETS collision_detector_bullet_plugin + moveit_acceleration_filter + moveit_acceleration_filter_parameters moveit_butterworth_filter - moveit_butterworth_parameters + moveit_butterworth_filter_parameters moveit_collision_detection moveit_collision_detection_bullet moveit_collision_detection_fcl @@ -115,6 +118,7 @@ ament_export_dependencies( moveit_msgs OCTOMAP octomap_msgs + osqp pluginlib random_numbers rclcpp @@ -138,6 +142,7 @@ ament_export_dependencies( pluginlib_export_plugin_description_file(moveit_core collision_detector_fcl_description.xml) pluginlib_export_plugin_description_file(moveit_core collision_detector_bullet_description.xml) pluginlib_export_plugin_description_file(moveit_core filter_plugin_butterworth.xml) +pluginlib_export_plugin_description_file(moveit_core filter_plugin_acceleration.xml) if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) diff --git a/moveit_core/filter_plugin_acceleration.xml b/moveit_core/filter_plugin_acceleration.xml new file mode 100644 index 0000000000..943ecf3f55 --- /dev/null +++ b/moveit_core/filter_plugin_acceleration.xml @@ -0,0 +1,8 @@ + + + + Limits acceleration of commands to generate smooth motion. + + + diff --git a/moveit_core/online_signal_smoothing/CMakeLists.txt b/moveit_core/online_signal_smoothing/CMakeLists.txt index 6fb01e2aad..f9b9cbaabe 100644 --- a/moveit_core/online_signal_smoothing/CMakeLists.txt +++ b/moveit_core/online_signal_smoothing/CMakeLists.txt @@ -33,10 +33,10 @@ set_target_properties(moveit_butterworth_filter PROPERTIES VERSION "${${PROJECT_NAME}_VERSION}" ) -generate_parameter_library(moveit_butterworth_parameters src/butterworth_parameters.yaml) +generate_parameter_library(moveit_butterworth_filter_parameters src/butterworth_parameters.yaml) target_link_libraries(moveit_butterworth_filter - moveit_butterworth_parameters + moveit_butterworth_filter_parameters moveit_robot_model moveit_smoothing_base ) @@ -45,18 +45,48 @@ ament_target_dependencies(moveit_butterworth_filter pluginlib ) +add_library(moveit_acceleration_filter SHARED + src/acceleration_filter.cpp +) +generate_export_header(moveit_acceleration_filter) +target_include_directories(moveit_acceleration_filter PUBLIC + $ +) +set_target_properties(moveit_acceleration_filter PROPERTIES VERSION + "${${PROJECT_NAME}_VERSION}" +) + +generate_parameter_library(moveit_acceleration_filter_parameters src/acceleration_filter_parameters.yaml) + +target_link_libraries(moveit_acceleration_filter + moveit_acceleration_filter_parameters + moveit_robot_state + moveit_smoothing_base + osqp::osqp +) +ament_target_dependencies(moveit_acceleration_filter + srdfdom # include dependency from moveit_robot_model + pluginlib +) + + # Installation install(DIRECTORY include/ DESTINATION include/moveit_core) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/moveit_smoothing_base_export.h DESTINATION include/moveit_core) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/moveit_butterworth_filter_export.h DESTINATION include/moveit_core) +install(FILES ${CMAKE_CURRENT_BINARY_DIR}/moveit_acceleration_filter_export.h DESTINATION include/moveit_core) # Testing -if(BUILD_TESTING) +if (BUILD_TESTING) find_package(ament_lint_auto REQUIRED) find_package(ament_cmake_gtest REQUIRED) # Lowpass filter unit test ament_add_gtest(test_butterworth_filter test/test_butterworth_filter.cpp) target_link_libraries(test_butterworth_filter moveit_butterworth_filter) -endif() + + # Acceleration filter unit test + ament_add_gtest(test_acceleration_filter test/test_acceleration_filter.cpp) + target_link_libraries(test_acceleration_filter moveit_acceleration_filter moveit_test_utils) +endif () diff --git a/moveit_core/online_signal_smoothing/README.md b/moveit_core/online_signal_smoothing/README.md new file mode 100644 index 0000000000..f6197856c1 --- /dev/null +++ b/moveit_core/online_signal_smoothing/README.md @@ -0,0 +1,12 @@ +### AccelerationLimitedPlugin +Applies smoothing by limiting the acceleration between consecutive commands. +The purpose of the plugin is to prevent the robot's acceleration limits from being violated by instantaneous changes +to the servo command topics. + +There are three cases considered for acceleration-limiting illustrated in the following figures: +![acceleration_limiting_diagram.png](res/acceleration_limiting_diagram.png) +In the figures, the red arrows show the displacement that will occur given the current velocity. The blue line shows the displacement between the current position and the desired position. The black dashed line shows the maximum possible displacements that are within the acceleration limits. The green line shows the acceleration commands that will be used by this acceleration-limiting plugin. + +- Figure A: The desired position is within the acceleration limits. The next commanded point will be exactly the desired point. +- Figure B: The line between the current position and the desired position intersects the acceleration limits, but the reference position is not within the bounds. The next commanded point will be the point on the displacement line that is closest to the reference. +- Figure C: Neither the displacement line intersects the acceleration limits nor does the reference point lie within the limits. In this case, the next commanded point will be the one that minimizes the robot's velocity while maintaining its direction. diff --git a/moveit_core/online_signal_smoothing/include/moveit/online_signal_smoothing/acceleration_filter.h b/moveit_core/online_signal_smoothing/include/moveit/online_signal_smoothing/acceleration_filter.h new file mode 100644 index 0000000000..361b14852e --- /dev/null +++ b/moveit_core/online_signal_smoothing/include/moveit/online_signal_smoothing/acceleration_filter.h @@ -0,0 +1,162 @@ +/********************************************************************* + * Software License Agreement (BSD License) + * + * Copyright (c) 2024, PickNik Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of PickNik Inc. nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *********************************************************************/ + +/* Author: Paul Gesel +Description: applies smoothing by limiting the acceleration between consecutive commands. +The purpose of the plugin is to prevent the robot's acceleration limits from being violated by instantaneous changes +to the servo command topics. + + In the diagrams below, the c-v lines show the displacement that will occur given the current velocity. The t-c lines + shows the displacement between the current position and the desired position. The dashed lines shows the maximum + possible displacements that are within the acceleration limits. The v-t lines shows the acceleration commands that + will be used by this acceleration-limiting plugin. The x point shows the position that will be used for each scenario. + +Scenario A: The desired position is within the acceleration limits. The next commanded point will be exactly the +desired point. + ________ + | | +c --|-----xt | + \__|__ v | + |________| + +Scenario B: The line between the current position and the desired position intersects the acceleration limits, but the +reference position is not within the bounds. The next commanded point will be the point on the displacement line that +is closest to the reference. + ________ + | | +c --|--------x------t + \__|__ v | + |________| + +Scenario C: Neither the displacement line intersects the acceleration limits nor does the reference point lie within +the limits. In this case, the next commanded point will be the one that minimizes the robot's velocity while +maintaining its direction. + ________ + | | +c --------x--- v | + \ | | + \ |________| + t + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace online_signal_smoothing +{ +MOVEIT_STRUCT_FORWARD(OSQPDataWrapper); + +// Plugin +class AccelerationLimitedPlugin : public SmoothingBaseClass +{ +public: + /** + * Initialize the acceleration based smoothing plugin + * @param node ROS node, used for parameter retrieval + * @param robot_model typically used to retrieve vel/accel/jerk limits + * @param num_joints number of actuated joints in the JointGroup Servo controls + * @return True if initialization was successful + */ + bool initialize(rclcpp::Node::SharedPtr node, moveit::core::RobotModelConstPtr robot_model, + size_t num_joints) override; + + /** + * Smooth the command signals for all DOF. This function limits the change in velocity using the acceleration + * specified in the robot model. + * @param positions array of joint position commands + * @param velocities array of joint velocity commands + * @param accelerations (unused) + * @return True if smoothing was successful + */ + bool doSmoothing(Eigen::VectorXd& positions, Eigen::VectorXd& velocities, Eigen::VectorXd& accelerations) override; + + /** + * Reset to a given joint state. This method must be called before doSmoothing. + * @param positions reset the filters to the joint positions + * @param velocities reset the filters to the joint velocities + * @param accelerations (unused) + * @return True if reset was successful + */ + bool reset(const Eigen::VectorXd& positions, const Eigen::VectorXd& velocities, + const Eigen::VectorXd& accelerations) override; + + /** + * memory allocated by osqp is freed in destructor + */ + ~AccelerationLimitedPlugin() override + { + if (osqp_workspace_ != nullptr) + { + osqp_cleanup(osqp_workspace_); + } + } + +private: + /** \brief Pointer to rclcpp node handle. */ + rclcpp::Node::SharedPtr node_; + /** \brief Parameters for plugin. */ + online_signal_smoothing::Params params_; + /** \brief The number of joints in the robot's planning group. */ + size_t num_joints_; + /** \brief Last velocities and positions received */ + Eigen::VectorXd last_velocities_; + Eigen::VectorXd last_positions_; + /** \brief Intermediate variables used in calculations */ + Eigen::VectorXd cur_acceleration_; + Eigen::VectorXd positions_offset_; + Eigen::VectorXd velocities_offset_; + /** \brief Extracted joint limits from robot model */ + Eigen::VectorXd max_acceleration_limits_; + Eigen::VectorXd min_acceleration_limits_; + /** \brief Pointer to robot model */ + moveit::core::RobotModelConstPtr robot_model_; + /** \brief Constraint matrix for optimization problem */ + Eigen::SparseMatrix constraints_sparse_; + /** \brief osqp types used for optimization problem */ + OSQPDataWrapperPtr osqp_data_; + OSQPWorkspace* osqp_workspace_ = nullptr; + OSQPSettings osqp_settings_; +}; +} // namespace online_signal_smoothing diff --git a/moveit_core/online_signal_smoothing/include/moveit/online_signal_smoothing/butterworth_filter.h b/moveit_core/online_signal_smoothing/include/moveit/online_signal_smoothing/butterworth_filter.h index bdcf6a7e5b..80212d2fd2 100644 --- a/moveit_core/online_signal_smoothing/include/moveit/online_signal_smoothing/butterworth_filter.h +++ b/moveit_core/online_signal_smoothing/include/moveit/online_signal_smoothing/butterworth_filter.h @@ -41,8 +41,7 @@ #include -// Auto-generated -#include +#include #include #include diff --git a/moveit_core/online_signal_smoothing/res/acceleration_limiting_diagram.png b/moveit_core/online_signal_smoothing/res/acceleration_limiting_diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..fe32aa78d99b0c1b70cba43f85c531f21f5bcb9b GIT binary patch literal 68588 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 U_Rcu`(-GtxPgg&ebxsLQ0P+aEpa1{> literal 0 HcmV?d00001 diff --git a/moveit_core/online_signal_smoothing/src/acceleration_filter.cpp b/moveit_core/online_signal_smoothing/src/acceleration_filter.cpp new file mode 100644 index 0000000000..3af91484f2 --- /dev/null +++ b/moveit_core/online_signal_smoothing/src/acceleration_filter.cpp @@ -0,0 +1,349 @@ +/********************************************************************* + * Software License Agreement (BSD License) + * + * Copyright (c) 2024, PickNik Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of PickNik Inc. nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *********************************************************************/ + +#include +#include +#include + +// Disable -Wold-style-cast because all _THROTTLE macros trigger this +#pragma GCC diagnostic ignored "-Wold-style-cast" + +namespace online_signal_smoothing +{ +rclcpp::Logger getLogger() +{ + return moveit::getLogger("AccelerationLimitedPlugin"); +} + +// The threshold below which any velocity or position difference is considered zero (rad and rad/s). +constexpr double COMMAND_DIFFERENCE_THRESHOLD = 1E-4; +// The scaling parameter alpha between the current point and commanded point must be less than 1.0 +constexpr double ALPHA_UPPER_BOUND = 1.0; +// The scaling parameter alpha must also be greater than 0.0 +constexpr double ALPHA_LOWER_BOUND = 0.0; + +/** \brief Wrapper struct to make memory management easier for using osqp's C sparse_matrix types */ +struct CSCWrapper +{ + /// row indices, size nzmax starting from 0 + std::vector row_indices; + /// column pointers (size n+1); col indices (size nzmax) + std::vector column_pointers; + /// holds the non-zero values in Compressed Sparse Column (CSC) form + std::vector elements; + /// osqp C sparse_matrix type + csc csc_sparse_matrix; + + CSCWrapper(Eigen::SparseMatrix& M) + { + M.makeCompressed(); + + csc_sparse_matrix.n = M.cols(); + csc_sparse_matrix.m = M.rows(); + row_indices.assign(M.innerSize(), 0); + csc_sparse_matrix.i = row_indices.data(); + column_pointers.assign(M.outerSize() + 1, 0); + csc_sparse_matrix.p = column_pointers.data(); + csc_sparse_matrix.nzmax = M.nonZeros(); + csc_sparse_matrix.nz = -1; + elements.assign(M.nonZeros(), 0.0); + csc_sparse_matrix.x = elements.data(); + + update(M); + } + + /// Update the the data point to by sparse_matrix without reallocating memory + void update(Eigen::SparseMatrix& M) + { + for (size_t ind = 0; ind < row_indices.size(); ++ind) + { + row_indices[ind] = M.innerIndexPtr()[ind]; + } + + for (size_t ind = 0; ind < column_pointers.size(); ++ind) + { + column_pointers[ind] = M.outerIndexPtr()[ind]; + } + for (size_t ind = 0; ind < elements.size(); ++ind) + { + elements[ind] = M.data().at(ind); + } + } +}; + +MOVEIT_STRUCT_FORWARD(OSQPDataWrapper); + +/** \brief Wrapper struct to make memory management easier for using osqp's C API */ +struct OSQPDataWrapper +{ + OSQPDataWrapper(Eigen::SparseMatrix& objective_sparse, Eigen::SparseMatrix& constraints_sparse) + : P{ objective_sparse }, A{ constraints_sparse } + { + data.n = objective_sparse.rows(); + data.m = constraints_sparse.rows(); + data.P = &P.csc_sparse_matrix; + q = Eigen::VectorXd::Zero(objective_sparse.rows()); + data.q = q.data(); + data.A = &A.csc_sparse_matrix; + l = Eigen::VectorXd::Zero(constraints_sparse.rows()); + data.l = l.data(); + u = Eigen::VectorXd::Zero(constraints_sparse.rows()); + data.u = u.data(); + } + + /// Update the constraint matrix A without reallocating memory + void updateA(OSQPWorkspace* work, Eigen::SparseMatrix& constraints_sparse) + { + constraints_sparse.makeCompressed(); + A.update(constraints_sparse); + osqp_update_A(work, A.elements.data(), OSQP_NULL, A.elements.size()); + } + + CSCWrapper P; + CSCWrapper A; + Eigen::VectorXd q; + Eigen::VectorXd l; + Eigen::VectorXd u; + OSQPData data{}; +}; + +bool AccelerationLimitedPlugin::initialize(rclcpp::Node::SharedPtr node, moveit::core::RobotModelConstPtr robot_model, + size_t num_joints) +{ + // copy inputs into member variables + node_ = node; + num_joints_ = num_joints; + robot_model_ = robot_model; + cur_acceleration_ = Eigen::VectorXd::Zero(num_joints); + + // get node parameters and store in member variables + auto param_listener = online_signal_smoothing::ParamListener(node_); + params_ = param_listener.get_params(); + + // get robot acceleration limits and store in member variables + auto joint_model_group = robot_model_->getJointModelGroup(params_.planning_group_name); + auto joint_bounds = joint_model_group->getActiveJointModelsBounds(); + min_acceleration_limits_ = Eigen::VectorXd::Zero(num_joints); + max_acceleration_limits_ = Eigen::VectorXd::Zero(num_joints); + size_t ind = 0; + for (const auto& joint_bound : joint_bounds) + { + for (const auto& variable_bound : *joint_bound) + { + if (variable_bound.acceleration_bounded_) + { + min_acceleration_limits_[ind] = variable_bound.min_acceleration_; + max_acceleration_limits_[ind] = variable_bound.max_acceleration_; + } + else + { + RCLCPP_ERROR(getLogger(), "The robot must have acceleration joint limits specified for all joints to " + "use AccelerationLimitedPlugin."); + return false; + } + } + ind++; + } + + // setup osqp optimization problem + Eigen::SparseMatrix objective_sparse(1, 1); + objective_sparse.insert(0, 0) = 1.0; + size_t num_constraints = num_joints + 1; + constraints_sparse_ = Eigen::SparseMatrix(num_constraints, 1); + for (size_t i = 0; i < num_constraints - 1; ++i) + { + constraints_sparse_.insert(i, 0) = 0; + } + constraints_sparse_.insert(num_constraints - 1, 0) = 0; + osqp_set_default_settings(&osqp_settings_); + osqp_settings_.warm_start = 0; + osqp_settings_.verbose = 0; + osqp_data_ = std::make_shared(objective_sparse, constraints_sparse_); + osqp_data_->q[0] = 0; + + if (osqp_setup(&osqp_workspace_, &osqp_data_->data, &osqp_settings_) != 0) + { + osqp_settings_.verbose = 1; + // call setup again with verbose enables to trigger error message printing + osqp_setup(&osqp_workspace_, &osqp_data_->data, &osqp_settings_); + RCLCPP_ERROR(getLogger(), "Failed to initialize osqp problem."); + return false; + } + + return true; +} + +double jointLimitAccelerationScalingFactor(const Eigen::VectorXd& accelerations, + const moveit::core::JointBoundsVector& joint_bounds) +{ + double min_scaling_factor = 1.0; + + // Now get the scaling factor from joint limits. + size_t idx = 0; + for (const auto& joint_bound : joint_bounds) + { + for (const auto& variable_bound : *joint_bound) + { + const auto& target_accel = accelerations(idx); + if (variable_bound.acceleration_bounded_ && target_accel != 0.0) + { + // Find the ratio of clamped acceleration to original acceleration + const auto bounded_vel = + std::clamp(target_accel, variable_bound.min_acceleration_, variable_bound.max_acceleration_); + double joint_scaling_factor = bounded_vel / target_accel; + min_scaling_factor = std::min(min_scaling_factor, joint_scaling_factor); + } + ++idx; + } + } + + return min_scaling_factor; +} + +inline bool updateData(const OSQPDataWrapperPtr& data, OSQPWorkspace* work, + Eigen::SparseMatrix& constraints_sparse, const Eigen::VectorXd& lower_bound, + const Eigen::VectorXd& upper_bound) +{ + data->updateA(work, constraints_sparse); + size_t num_constraints = constraints_sparse.rows(); + data->u.block(0, 0, num_constraints - 1, 1) = upper_bound; + data->l.block(0, 0, num_constraints - 1, 1) = lower_bound; + data->u[num_constraints - 1] = ALPHA_UPPER_BOUND; + data->l[num_constraints - 1] = ALPHA_LOWER_BOUND; + return 0 == osqp_update_bounds(work, data->l.data(), data->u.data()); +} + +bool AccelerationLimitedPlugin::doSmoothing(Eigen::VectorXd& positions, Eigen::VectorXd& velocities, + Eigen::VectorXd& /* unused */) +{ + const size_t num_positions = velocities.size(); + if (num_positions != num_joints_) + { + RCLCPP_ERROR_THROTTLE( + getLogger(), *node_->get_clock(), 1000, + "The length of the joint positions parameter is not equal to the number of joints, expected %zu got %zu.", + num_joints_, num_positions); + return false; + } + else if (last_positions_.size() != positions.size()) + { + RCLCPP_ERROR_THROTTLE(getLogger(), *node_->get_clock(), 1000, + "The length of the last joint positions not equal to the current, expected %zu got %zu. Make " + "sure the reset was called.", + last_positions_.size(), positions.size()); + return false; + } + + // formulate a quadratic program to find the best new reference point subject to the robot's acceleration limits + // p_c: robot's current position + // v_c: robot's current velocity + // p_t: robot's target position + // acc: acceleration to be applied + // p_n: next position + // dt: time step + // p_n_hat: parameterize solution to be along the line from p_c to p_t + // p_n_hat = p_t*alpha + p_c*(1-alpha) + // define constraints + // p_c + v_c*dt + acc_min*dt^2 < p_n_hat < p_c + v_c*dt + acc_max*dt^2 + // p_c + v_c*dt -p_t + acc_min*dt^2 < (p_c-p_t)alpha < p_c + v_c*dt -p_t + acc_max*dt^2 + // 0 < alpha < 1 + // define optimization + // opt ||alpha|| + // s.t. constraints + // p_n = p_t*alpha + p_c*(1-alpha) + + double& update_period = params_.update_period; + size_t num_constraints = num_joints_ + 1; + positions_offset_ = last_positions_ - positions; + velocities_offset_ = last_velocities_ - velocities; + for (size_t i = 0; i < num_constraints - 1; ++i) + { + constraints_sparse_.coeffRef(i, 0) = positions_offset_[i]; + } + constraints_sparse_.coeffRef(num_constraints - 1, 0) = 1; + Eigen::VectorXd vel_point = last_positions_ + last_velocities_ * update_period; + Eigen::VectorXd upper_bound = vel_point - positions + max_acceleration_limits_ * (update_period * update_period); + Eigen::VectorXd lower_bound = vel_point - positions + min_acceleration_limits_ * (update_period * update_period); + if (!updateData(osqp_data_, osqp_workspace_, constraints_sparse_, lower_bound, upper_bound)) + { + RCLCPP_ERROR_THROTTLE(getLogger(), *node_->get_clock(), 1000, + "failed to set osqp_update_bounds. Make sure the robot's acceleration limits are valid"); + return false; + } + + if (positions_offset_.norm() < COMMAND_DIFFERENCE_THRESHOLD && + velocities_offset_.norm() < COMMAND_DIFFERENCE_THRESHOLD) + { + positions = last_positions_; + velocities = last_velocities_; + } + else if (osqp_solve(osqp_workspace_) == 0 && + osqp_workspace_->solution->x[0] >= ALPHA_LOWER_BOUND - osqp_settings_.eps_abs && + osqp_workspace_->solution->x[0] <= ALPHA_UPPER_BOUND + osqp_settings_.eps_abs) + { + double alpha = osqp_workspace_->solution->x[0]; + positions = alpha * last_positions_ + (1.0 - alpha) * positions.eval(); + velocities = (positions - last_positions_) / update_period; + } + else + { + auto joint_model_group = robot_model_->getJointModelGroup(params_.planning_group_name); + auto joint_bounds = joint_model_group->getActiveJointModelsBounds(); + cur_acceleration_ = -(last_velocities_) / update_period; + cur_acceleration_ *= jointLimitAccelerationScalingFactor(cur_acceleration_, joint_bounds); + velocities = last_velocities_ + cur_acceleration_ * update_period; + positions = last_positions_ + velocities * update_period; + } + + last_velocities_ = velocities; + last_positions_ = positions; + + return true; +} + +bool AccelerationLimitedPlugin::reset(const Eigen::VectorXd& positions, const Eigen::VectorXd& velocities, + const Eigen::VectorXd& /* unused */) +{ + last_velocities_ = velocities; + last_positions_ = positions; + cur_acceleration_ = Eigen::VectorXd::Zero(num_joints_); + + return true; +} + +} // namespace online_signal_smoothing + +#include + +PLUGINLIB_EXPORT_CLASS(online_signal_smoothing::AccelerationLimitedPlugin, online_signal_smoothing::SmoothingBaseClass) diff --git a/moveit_core/online_signal_smoothing/src/acceleration_filter_parameters.yaml b/moveit_core/online_signal_smoothing/src/acceleration_filter_parameters.yaml new file mode 100644 index 0000000000..92575e5059 --- /dev/null +++ b/moveit_core/online_signal_smoothing/src/acceleration_filter_parameters.yaml @@ -0,0 +1,16 @@ +online_signal_smoothing: + update_period: { + type: double, + description: "The expected time in seconds between calls to `doSmoothing` method.", + read_only: true, + validation: { + gt<>: 0.0 + } + } + planning_group_name: { + type: string, + read_only: true, + description: "The name of the MoveIt planning group of the robot \ + This parameter does not have a default value and \ + must be passed to the node during launch time." + } diff --git a/moveit_core/online_signal_smoothing/test/test_acceleration_filter.cpp b/moveit_core/online_signal_smoothing/test/test_acceleration_filter.cpp new file mode 100644 index 0000000000..aeff731f2a --- /dev/null +++ b/moveit_core/online_signal_smoothing/test/test_acceleration_filter.cpp @@ -0,0 +1,207 @@ +/********************************************************************* + * Software License Agreement (BSD License) + * + * Copyright (c) 2024, PickNik Inc. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of PickNik Inc. nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + *********************************************************************/ + +#include +#include +#include + +constexpr std::string_view PLANNING_GROUP_NAME = "panda_arm"; +constexpr size_t PANDA_NUM_JOINTS = 7u; +constexpr std::string_view ROBOT_MODEL = "panda"; + +class AccelerationFilterTest : public testing::Test +{ +protected: + void SetUp() override + { + robot_model_ = moveit::core::loadTestingRobotModel(ROBOT_MODEL.data()); + }; + + void setLimits(std::optional acceleration_limits) + { + const std::vector joint_models = robot_model_->getJointModels(); + auto joint_model_group = robot_model_->getJointModelGroup(PLANNING_GROUP_NAME.data()); + size_t ind = 0; + for (auto& joint_model : joint_models) + { + if (!joint_model_group->hasJointModel(joint_model->getName())) + { + continue; + } + std::vector joint_bounds_msg(joint_model->getVariableBoundsMsg()); + for (auto& joint_bound : joint_bounds_msg) + { + joint_bound.has_acceleration_limits = acceleration_limits.has_value(); + if (joint_bound.has_acceleration_limits) + { + joint_bound.max_acceleration = acceleration_limits.value()[ind]; + } + } + joint_model->setVariableBounds(joint_bounds_msg); + ind++; + } + } + +protected: + moveit::core::RobotModelPtr robot_model_; +}; + +TEST_F(AccelerationFilterTest, FilterInitialize) +{ + online_signal_smoothing::AccelerationLimitedPlugin filter; + rclcpp::Node::SharedPtr node = std::make_shared("AccelerationFilterTest"); + + // fail because the update_period parameter is not set + EXPECT_THROW(filter.initialize(node, robot_model_, PANDA_NUM_JOINTS), + rclcpp::exceptions::ParameterUninitializedException); + + node = std::make_shared("AccelerationFilterTest"); + node->declare_parameter("planning_group_name", PLANNING_GROUP_NAME.data()); + node->declare_parameter("update_period", 0.01); + + // fail because the number of joints is wrong + EXPECT_FALSE(filter.initialize(node, robot_model_, 3u)); + + // fail because the robot does not have acceleration limits + setLimits({}); + EXPECT_FALSE(filter.initialize(node, robot_model_, PANDA_NUM_JOINTS)); + + // succeed with acceleration limits + Eigen::VectorXd acceleration_limits = 1.2 * Eigen::VectorXd::Ones(PANDA_NUM_JOINTS); + setLimits(acceleration_limits); + EXPECT_TRUE(filter.initialize(node, robot_model_, PANDA_NUM_JOINTS)); +} + +TEST_F(AccelerationFilterTest, FilterDoSmooth) +{ + online_signal_smoothing::AccelerationLimitedPlugin filter; + + rclcpp::Node::SharedPtr node = std::make_shared("AccelerationFilterTest"); + node->declare_parameter("planning_group_name", PLANNING_GROUP_NAME.data()); + const double update_period = 1.0; + node->declare_parameter("update_period", update_period); + Eigen::VectorXd acceleration_limits = 1.2 * Eigen::VectorXd::Ones(PANDA_NUM_JOINTS); + setLimits(acceleration_limits); + + EXPECT_TRUE(filter.initialize(node, robot_model_, PANDA_NUM_JOINTS)); + + // fail when called with the wrong number of joints + Eigen::VectorXd position = Eigen::VectorXd::Zero(5); + Eigen::VectorXd velocity = Eigen::VectorXd::Zero(5); + Eigen::VectorXd acceleration = Eigen::VectorXd::Zero(5); + EXPECT_FALSE(filter.doSmoothing(position, velocity, acceleration)); + + // fail because reset was not called + position = Eigen::VectorXd::Zero(PANDA_NUM_JOINTS); + velocity = Eigen::VectorXd::Zero(PANDA_NUM_JOINTS); + acceleration = Eigen::VectorXd::Zero(PANDA_NUM_JOINTS); + EXPECT_FALSE(filter.doSmoothing(position, velocity, acceleration)); + + // succeed + filter.reset(Eigen::VectorXd::Zero(PANDA_NUM_JOINTS), Eigen::VectorXd::Zero(PANDA_NUM_JOINTS), + Eigen::VectorXd::Zero(PANDA_NUM_JOINTS)); + EXPECT_TRUE(filter.doSmoothing(position, velocity, acceleration)); + + // succeed: apply acceleration limits + filter.reset(Eigen::VectorXd::Zero(PANDA_NUM_JOINTS), Eigen::VectorXd::Zero(PANDA_NUM_JOINTS), + Eigen::VectorXd::Zero(PANDA_NUM_JOINTS)); + position.array() = 3.0; + EXPECT_TRUE(filter.doSmoothing(position, velocity, acceleration)); + EXPECT_TRUE((position.array() - update_period * update_period * acceleration_limits.array()).matrix().norm() < 1E-3); + + // succeed: apply acceleration limits + position.array() = 0.9; + filter.reset(position * 0, velocity * 0, acceleration * 0); + EXPECT_TRUE(filter.doSmoothing(position, velocity, acceleration)); + EXPECT_TRUE((position.array() - 0.9).matrix().norm() < 1E-3); + + // succeed: slow down + velocity = 10 * Eigen::VectorXd::Ones(PANDA_NUM_JOINTS); + filter.reset(Eigen::VectorXd::Zero(PANDA_NUM_JOINTS), velocity, Eigen::VectorXd::Zero(PANDA_NUM_JOINTS)); + position.array() = 0.01; + Eigen::VectorXd expected_offset = + velocity.array() * update_period - update_period * update_period * acceleration_limits.array(); + EXPECT_TRUE(filter.doSmoothing(position, velocity, acceleration)); + EXPECT_TRUE((velocity * update_period - expected_offset).norm() < 1E-3); +} + +TEST_F(AccelerationFilterTest, FilterBadAccelerationConfig) +{ + online_signal_smoothing::AccelerationLimitedPlugin filter; + + rclcpp::Node::SharedPtr node = std::make_shared("AccelerationFilterTest"); + node->declare_parameter("planning_group_name", PLANNING_GROUP_NAME.data()); + const double update_period = 0.1; + node->declare_parameter("update_period", update_period); + Eigen::VectorXd acceleration_limits = -1.0 * Eigen::VectorXd::Ones(PANDA_NUM_JOINTS); + setLimits(acceleration_limits); + EXPECT_TRUE(filter.initialize(node, robot_model_, PANDA_NUM_JOINTS)); + Eigen::VectorXd position = Eigen::VectorXd::Zero(PANDA_NUM_JOINTS); + Eigen::VectorXd velocity = Eigen::VectorXd::Zero(PANDA_NUM_JOINTS); + Eigen::VectorXd acceleration = Eigen::VectorXd::Zero(PANDA_NUM_JOINTS); + EXPECT_TRUE(filter.reset(position, velocity, acceleration)); + EXPECT_FALSE(filter.doSmoothing(position, velocity, acceleration)); +} + +TEST_F(AccelerationFilterTest, FilterDoSmoothRandomized) +{ + online_signal_smoothing::AccelerationLimitedPlugin filter; + rclcpp::Node::SharedPtr node = std::make_shared("AccelerationFilterTest"); + const double update_period = 0.1; + node->declare_parameter("planning_group_name", PLANNING_GROUP_NAME.data()); + node->declare_parameter("update_period", update_period); + Eigen::VectorXd acceleration_limits = 1.2 * (1.0 + Eigen::VectorXd::Random(PANDA_NUM_JOINTS).array()); + setLimits(acceleration_limits); + EXPECT_TRUE(filter.initialize(node, robot_model_, PANDA_NUM_JOINTS)); + + for (size_t iter = 0; iter < 64; ++iter) + { + acceleration_limits = 1.2 * (1.0 + Eigen::VectorXd::Random(PANDA_NUM_JOINTS).array()); + setLimits(acceleration_limits); + EXPECT_TRUE(filter.initialize(node, robot_model_, PANDA_NUM_JOINTS)); + filter.reset(Eigen::VectorXd::Zero(PANDA_NUM_JOINTS), Eigen::VectorXd::Zero(PANDA_NUM_JOINTS), + Eigen::VectorXd::Zero(PANDA_NUM_JOINTS)); + Eigen::VectorXd position = Eigen::VectorXd::Random(PANDA_NUM_JOINTS); + Eigen::VectorXd velocity = Eigen::VectorXd::Random(PANDA_NUM_JOINTS); + Eigen::VectorXd acceleration = Eigen::VectorXd::Random(PANDA_NUM_JOINTS); + EXPECT_TRUE(filter.doSmoothing(position, velocity, acceleration)); + } +} + +int main(int argc, char** argv) +{ + testing::InitGoogleTest(&argc, argv); + rclcpp::init(argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/moveit_core/package.xml b/moveit_core/package.xml index 2b22cc7cab..bf07bc6849 100644 --- a/moveit_core/package.xml +++ b/moveit_core/package.xml @@ -31,8 +31,8 @@ boost bullet common_interfaces - eigen_stl_containers eigen + eigen_stl_containers generate_parameter_library geometric_shapes geometry_msgs @@ -41,8 +41,9 @@ libfcl-dev moveit_common moveit_msgs - octomap_msgs octomap + octomap_msgs + osqp_vendor pluginlib random_numbers rclcpp @@ -52,14 +53,14 @@ shape_msgs srdfdom std_msgs + tf2 tf2_eigen tf2_geometry_msgs tf2_kdl - tf2 trajectory_msgs urdf - urdfdom_headers urdfdom + urdfdom_headers visualization_msgs python3-sphinx-rtd-theme diff --git a/moveit_ros/moveit_servo/config/panda_simulated_config.yaml b/moveit_ros/moveit_servo/config/panda_simulated_config.yaml index 878f27b141..ac3d0eb068 100644 --- a/moveit_ros/moveit_servo/config/panda_simulated_config.yaml +++ b/moveit_ros/moveit_servo/config/panda_simulated_config.yaml @@ -28,7 +28,7 @@ publish_joint_accelerations: false ## Plugins for smoothing outgoing commands use_smoothing: true -smoothing_filter_plugin_name: "online_signal_smoothing::ButterworthFilterPlugin" +smoothing_filter_plugin_name: "online_signal_smoothing::AccelerationLimitedPlugin" # If is_primary_planning_scene_monitor is set to true, the Servo server's PlanningScene advertises the /get_planning_scene service, # which other nodes can use as a source for information about the planning environment. diff --git a/moveit_ros/moveit_servo/launch/demo_joint_jog.launch.py b/moveit_ros/moveit_servo/launch/demo_joint_jog.launch.py index 77cecddbd6..62404efc2a 100644 --- a/moveit_ros/moveit_servo/launch/demo_joint_jog.launch.py +++ b/moveit_ros/moveit_servo/launch/demo_joint_jog.launch.py @@ -10,7 +10,7 @@ def generate_launch_description(): moveit_config = ( MoveItConfigsBuilder("moveit_resources_panda") .robot_description(file_path="config/panda.urdf.xacro") - .joint_limits() + .joint_limits(file_path="config/hard_joint_limits.yaml") .robot_description_kinematics() .to_moveit_configs() ) diff --git a/moveit_ros/moveit_servo/launch/demo_pose.launch.py b/moveit_ros/moveit_servo/launch/demo_pose.launch.py index aef3d2bda0..f2a77201e9 100644 --- a/moveit_ros/moveit_servo/launch/demo_pose.launch.py +++ b/moveit_ros/moveit_servo/launch/demo_pose.launch.py @@ -10,7 +10,7 @@ def generate_launch_description(): moveit_config = ( MoveItConfigsBuilder("moveit_resources_panda") .robot_description(file_path="config/panda.urdf.xacro") - .joint_limits() + .joint_limits(file_path="config/hard_joint_limits.yaml") .robot_description_kinematics() .to_moveit_configs() ) diff --git a/moveit_ros/moveit_servo/launch/demo_ros_api.launch.py b/moveit_ros/moveit_servo/launch/demo_ros_api.launch.py index 2768ea9268..ac008bfb66 100644 --- a/moveit_ros/moveit_servo/launch/demo_ros_api.launch.py +++ b/moveit_ros/moveit_servo/launch/demo_ros_api.launch.py @@ -12,8 +12,7 @@ def generate_launch_description(): moveit_config = ( MoveItConfigsBuilder("moveit_resources_panda") .robot_description(file_path="config/panda.urdf.xacro") - .joint_limits() - .robot_description_kinematics() + .joint_limits(file_path="config/hard_joint_limits.yaml") .to_moveit_configs() ) @@ -29,9 +28,9 @@ def generate_launch_description(): .to_dict() } - # This filter parameter should be >1. Increase it for greater smoothing but slower motion. - low_pass_filter_coeff = {"butterworth_filter_coeff": 1.5} - + # This sets the update rate and planning group name for the acceleration limiting filter. + acceleration_filter_update_period = {"update_period": 0.01} + planning_group_name = {"planning_group_name": "panda_arm"} # RViz rviz_config_file = ( get_package_share_directory("moveit_servo") @@ -98,7 +97,8 @@ def generate_launch_description(): name="servo_node", parameters=[ servo_params, - low_pass_filter_coeff, + acceleration_filter_update_period, + planning_group_name, moveit_config.robot_description, moveit_config.robot_description_semantic, moveit_config.robot_description_kinematics, @@ -129,7 +129,8 @@ def generate_launch_description(): name="servo_node", parameters=[ servo_params, - low_pass_filter_coeff, + acceleration_filter_update_period, + planning_group_name, moveit_config.robot_description, moveit_config.robot_description_semantic, moveit_config.robot_description_kinematics, diff --git a/moveit_ros/moveit_servo/launch/demo_twist.launch.py b/moveit_ros/moveit_servo/launch/demo_twist.launch.py index 1230aa168c..4c4867fb82 100644 --- a/moveit_ros/moveit_servo/launch/demo_twist.launch.py +++ b/moveit_ros/moveit_servo/launch/demo_twist.launch.py @@ -10,7 +10,7 @@ def generate_launch_description(): moveit_config = ( MoveItConfigsBuilder("moveit_resources_panda") .robot_description(file_path="config/panda.urdf.xacro") - .joint_limits() + .joint_limits(file_path="config/hard_joint_limits.yaml") .robot_description_kinematics() .to_moveit_configs() ) diff --git a/moveit_ros/moveit_servo/src/servo.cpp b/moveit_ros/moveit_servo/src/servo.cpp index 09ef4a477f..9f44cb5462 100644 --- a/moveit_ros/moveit_servo/src/servo.cpp +++ b/moveit_ros/moveit_servo/src/servo.cpp @@ -514,16 +514,6 @@ KinematicState Servo::getNextJointState(const moveit::core::RobotStatePtr& robot // Compute the joint velocities required to reach positions target_state.velocities = joint_position_delta / servo_params_.publish_period; - // Apply smoothing to the positions if a smoother was provided. - doSmoothing(target_state); - - // Apply collision scaling to the joint position delta - target_state.positions = - current_state.positions + collision_velocity_scale_ * (target_state.positions - current_state.positions); - - // Compute velocities based on smoothed joint positions - target_state.velocities = (target_state.positions - current_state.positions) / servo_params_.publish_period; - // Scale down the velocity based on joint velocity limit or user defined scaling if applicable. const double joint_velocity_limit_scale = jointLimitVelocityScalingFactor( target_state.velocities, joint_bounds, servo_params_.override_velocity_scaling_factor); @@ -536,6 +526,16 @@ KinematicState Servo::getNextJointState(const moveit::core::RobotStatePtr& robot // Adjust joint position based on scaled down velocity target_state.positions = current_state.positions + (target_state.velocities * servo_params_.publish_period); + // Apply smoothing to the positions if a smoother was provided. + doSmoothing(target_state); + + // Apply collision scaling to the joint position delta + target_state.positions = + current_state.positions + collision_velocity_scale_ * (target_state.positions - current_state.positions); + + // Compute velocities based on smoothed joint positions + target_state.velocities = (target_state.positions - current_state.positions) / servo_params_.publish_period; + // Check if any joints are going past joint position limits const std::vector joints_to_halt = jointsToHalt(target_state.positions, target_state.velocities, joint_bounds, servo_params_.joint_limit_margins); @@ -548,6 +548,9 @@ KinematicState Servo::getNextJointState(const moveit::core::RobotStatePtr& robot } } + // Update internal state of filter with final calculated command. + resetSmoothing(target_state); + return target_state; } @@ -678,6 +681,7 @@ std::pair Servo::smoothHalt(const KinematicState& halt_sta } } + resetSmoothing(target_state); return std::make_pair(stopped, target_state); }