diff --git a/CMakeLists.txt b/CMakeLists.txt
index 8eca517..2176f74 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -51,12 +51,18 @@ generate_parameter_library(
src/torque_feedback_controller.yaml
)
+generate_parameter_library(
+ twist_broadcaster_parameters
+ src/twist_broadcaster.yaml
+)
+
add_library(
${PROJECT_NAME}
SHARED
src/cartesian_controller.cpp
src/pose_broadcaster.cpp
src/torque_feedback_controller.cpp
+ src/twist_broadcaster.cpp
)
target_include_directories(${PROJECT_NAME}
@@ -78,6 +84,7 @@ target_link_libraries(${PROJECT_NAME}
cartesian_impedance_controller_parameters
pose_broadcaster_parameters
torque_feedback_controller_parameters
+ twist_broadcaster_parameters
)
diff --git a/crisp_controllers.xml b/crisp_controllers.xml
index fd16063..0b0d64e 100644
--- a/crisp_controllers.xml
+++ b/crisp_controllers.xml
@@ -17,4 +17,10 @@
Torque feedback controller for robotic manipulators that responds to external torques with PD control and friction compensation.
+
+
+ Simple broadcaster for the twist velocity.
+
+
diff --git a/include/crisp_controllers/pose_broadcaster.hpp b/include/crisp_controllers/pose_broadcaster.hpp
index 11587cc..324541d 100644
--- a/include/crisp_controllers/pose_broadcaster.hpp
+++ b/include/crisp_controllers/pose_broadcaster.hpp
@@ -64,6 +64,7 @@ class PoseBroadcaster
{"JointModelRUBX", "JointModelRUBY", "JointModelRUBZ"};
Eigen::VectorXd q;
+ rclcpp::Time last_publish_time_;
};
} // namespace crisp_controllers
diff --git a/include/crisp_controllers/twist_broadcaster.hpp b/include/crisp_controllers/twist_broadcaster.hpp
new file mode 100644
index 0000000..8838a6c
--- /dev/null
+++ b/include/crisp_controllers/twist_broadcaster.hpp
@@ -0,0 +1,71 @@
+#pragma once
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+
+using CallbackReturn =
+ rclcpp_lifecycle::node_interfaces::LifecycleNodeInterface::CallbackReturn;
+
+namespace crisp_controllers {
+
+class TwistBroadcaster
+ : public controller_interface::ControllerInterface {
+public:
+ [[nodiscard]] controller_interface::InterfaceConfiguration
+ command_interface_configuration() const override;
+ [[nodiscard]] controller_interface::InterfaceConfiguration
+ state_interface_configuration() const override;
+ controller_interface::return_type
+ update(const rclcpp::Time &time, const rclcpp::Duration &period) override;
+ CallbackReturn on_init() override;
+ CallbackReturn
+ on_configure(const rclcpp_lifecycle::State &previous_state) override;
+ CallbackReturn
+ on_activate(const rclcpp_lifecycle::State &previous_state) override;
+ CallbackReturn
+ on_deactivate(const rclcpp_lifecycle::State &previous_state) override;
+
+private:
+ std::shared_ptr params_listener_;
+ twist_broadcaster::Params params_;
+
+ rclcpp::Publisher::SharedPtr twist_publisher_;
+ std::shared_ptr>
+ realtime_twist_publisher_;
+
+
+ std::string end_effector_frame_;
+ int end_effector_frame_id;
+
+ pinocchio::Model model_;
+ pinocchio::Data data_;
+
+ /** @brief Allowed type of joints **/
+ const std::unordered_set> allowed_joint_types = {
+ "JointModelRX",
+ "JointModelRY",
+ "JointModelRZ",
+ "JointModelRevoluteUnaligned",
+ "JointModelRUBX",
+ "JointModelRUBY",
+ "JointModelRUBZ",
+ };
+ /** @brief Continous joint types that should be considered separetly. **/
+ const std::unordered_set> continous_joint_types =
+ {"JointModelRUBX", "JointModelRUBY", "JointModelRUBZ"};
+
+ Eigen::VectorXd q;
+ Eigen::VectorXd q_dot;
+ rclcpp::Time last_publish_time_;
+};
+
+} // namespace crisp_controllers
diff --git a/src/pose_broadcaster.cpp b/src/pose_broadcaster.cpp
index f0763cb..7de3bfb 100644
--- a/src/pose_broadcaster.cpp
+++ b/src/pose_broadcaster.cpp
@@ -62,7 +62,15 @@ PoseBroadcaster::update(const rclcpp::Time &time,
auto current_quaternion =
Eigen::Quaterniond(current_pose.rotation());
- if (realtime_pose_publisher_ && realtime_pose_publisher_->trylock())
+ // Decide whether to publish the pose or not
+ bool should_publish = true;
+ if (params_.publish_frequency > 0.0) {
+ auto time_since_last = time - last_publish_time_;
+ auto min_interval = rclcpp::Duration::from_seconds(1.0 / params_.publish_frequency);
+ should_publish = time_since_last >= min_interval;
+ }
+
+ if (should_publish && realtime_pose_publisher_ && realtime_pose_publisher_->trylock())
{
auto & pose_msg = realtime_pose_publisher_->msg_;
@@ -76,6 +84,7 @@ PoseBroadcaster::update(const rclcpp::Time &time,
pose_msg.pose.orientation.z = current_quaternion.z();
pose_msg.pose.orientation.w = current_quaternion.w();
realtime_pose_publisher_->unlockAndPublish();
+ last_publish_time_ = time;
}
return controller_interface::return_type::OK;
@@ -158,6 +167,8 @@ CallbackReturn PoseBroadcaster::on_configure(
realtime_pose_publisher_ =
std::make_shared>(
pose_publisher_);
+
+ last_publish_time_ = this->get_node()->now();
return CallbackReturn::SUCCESS;
}
diff --git a/src/pose_broadcaster.yaml b/src/pose_broadcaster.yaml
index 7d882f0..3c687d6 100644
--- a/src/pose_broadcaster.yaml
+++ b/src/pose_broadcaster.yaml
@@ -9,3 +9,7 @@ pose_broadcaster:
base_frame:
type: string
description: "Name of the base frame"
+ publish_frequency:
+ type: double
+ default_value: 250.0
+ description: "Publishing frequency in Hz (0.0 means no throttling)"
diff --git a/src/twist_broadcaster.cpp b/src/twist_broadcaster.cpp
new file mode 100644
index 0000000..1724db3
--- /dev/null
+++ b/src/twist_broadcaster.cpp
@@ -0,0 +1,193 @@
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace std::chrono_literals;
+
+namespace crisp_controllers {
+
+controller_interface::InterfaceConfiguration
+TwistBroadcaster::command_interface_configuration() const {
+ controller_interface::InterfaceConfiguration config;
+ config.type = controller_interface::interface_configuration_type::NONE;
+ return config;
+}
+
+controller_interface::InterfaceConfiguration
+TwistBroadcaster::state_interface_configuration() const {
+ controller_interface::InterfaceConfiguration config;
+ config.type = controller_interface::interface_configuration_type::INDIVIDUAL;
+ for (const auto &joint_name : params_.joints) {
+ config.names.push_back(joint_name + "/position");
+ config.names.push_back(joint_name + "/velocity");
+ }
+ return config;
+}
+
+controller_interface::return_type
+TwistBroadcaster::update(const rclcpp::Time &time,
+ const rclcpp::Duration & /*period*/) {
+
+ size_t num_joints = params_.joints.size();
+ Eigen::VectorXd q_pin = Eigen::VectorXd::Zero(model_.nq);
+ Eigen::VectorXd q_dot_pin = Eigen::VectorXd::Zero(model_.nv);
+
+ for (size_t i = 0; i < num_joints; i++) {
+
+ auto joint_name = params_.joints[i];
+ auto joint_id = model_.getJointId(joint_name);
+ auto joint = model_.joints[joint_id];
+
+ q[i] = state_interfaces_[i*2].get_value();
+ q_dot[i] = state_interfaces_[i*2+1].get_value();
+
+ if (continous_joint_types.count(joint.shortname())) { // Then we are handling a continous joint that is SO(2)
+ q_pin[joint.idx_q()] = std::cos(q[i]);
+ q_pin[joint.idx_q()+1] = std::sin(q[i]);
+ q_dot_pin[joint.idx_v()] = q_dot[i];
+ } else {
+ q_pin[joint.idx_q()] = q[i];
+ q_dot_pin[joint.idx_v()] = q_dot[i];
+ }
+ }
+
+ pinocchio::forwardKinematics(model_, data_, q_pin, q_dot_pin);
+ pinocchio::updateFramePlacements(model_, data_);
+
+ auto current_velocity = pinocchio::getFrameVelocity(model_, data_, end_effector_frame_id);
+
+ // Decide whether to publish the twist or not
+ bool should_publish = true;
+ if (params_.publish_frequency > 0.0) {
+ auto time_since_last = time - last_publish_time_;
+ auto min_interval = rclcpp::Duration::from_seconds(1.0 / params_.publish_frequency);
+ should_publish = time_since_last >= min_interval;
+ }
+
+ if (should_publish && realtime_twist_publisher_ && realtime_twist_publisher_->trylock())
+ {
+ auto & twist_msg = realtime_twist_publisher_->msg_;
+
+ twist_msg.header.stamp = time;
+ twist_msg.header.frame_id = params_.end_effector_frame;
+ twist_msg.twist.linear.x = current_velocity.linear()[0];
+ twist_msg.twist.linear.y = current_velocity.linear()[1];
+ twist_msg.twist.linear.z = current_velocity.linear()[2];
+ twist_msg.twist.angular.x = current_velocity.angular()[0];
+ twist_msg.twist.angular.y = current_velocity.angular()[1];
+ twist_msg.twist.angular.z = current_velocity.angular()[2];
+ realtime_twist_publisher_->unlockAndPublish();
+ last_publish_time_ = time;
+ }
+
+ return controller_interface::return_type::OK;
+}
+
+CallbackReturn TwistBroadcaster::on_init() {
+ // Initialize parameters
+ params_listener_ =
+ std::make_shared(get_node());
+ params_listener_->refresh_dynamic_parameters();
+ params_ = params_listener_->get_params();
+
+ return CallbackReturn::SUCCESS;
+}
+
+CallbackReturn TwistBroadcaster::on_configure(
+ const rclcpp_lifecycle::State & /*previous_state*/) {
+
+ auto parameters_client = std::make_shared(
+ get_node(), "robot_state_publisher");
+ parameters_client->wait_for_service();
+
+ auto future = parameters_client->get_parameters({"robot_description"});
+ auto result = future.get();
+
+ std::string robot_description_;
+ if (!result.empty()) {
+ robot_description_ = result[0].value_to_string();
+ } else {
+ RCLCPP_ERROR(get_node()->get_logger(),
+ "Failed to get robot_description parameter.");
+ return CallbackReturn::ERROR;
+ }
+
+ pinocchio::Model raw_model_;
+ pinocchio::urdf::buildModelFromXML(robot_description_, raw_model_);
+
+ RCLCPP_INFO(get_node()->get_logger(), "Checking available joints in model:");
+ for (int joint_id = 0; joint_id < raw_model_.njoints; joint_id++) {
+ RCLCPP_INFO_STREAM(get_node()->get_logger(), "Joint " << joint_id << " with name " << raw_model_.names[joint_id] << " is of type " << raw_model_.joints[joint_id].shortname());
+ }
+
+ // First we check that the passed joints exist in the kineatic tree
+ for (auto& joint : params_.joints) {
+ if (not raw_model_.existJointName(joint)) {
+ RCLCPP_ERROR_STREAM(get_node()->get_logger(), "Failed to configure because " << joint << " is not part of the kinematic tree but it has been passed in the parameters.");
+ return CallbackReturn::ERROR;
+ }
+ }
+ RCLCPP_INFO(get_node()->get_logger(), "All joints passed in the parameters exist in the kinematic tree of the URDF.");
+ RCLCPP_INFO_STREAM(get_node()->get_logger(), "Removing the rest of the joints that are not used: ");
+ // Now we fix all joints that are not referenced in the tree
+ std::vector list_of_joints_to_lock_by_id;
+ for (auto& joint : raw_model_.names) {
+ if (std::find(params_.joints.begin(), params_.joints.end(), joint) == params_.joints.end() and joint != "universe") {
+ RCLCPP_INFO_STREAM(get_node()->get_logger(), "Joint " << joint << " is not used, removing it from the model.");
+ list_of_joints_to_lock_by_id.push_back(raw_model_.getJointId(joint));
+ }
+ }
+
+ Eigen::VectorXd q_locked = Eigen::VectorXd::Zero(raw_model_.nq);
+ model_ = pinocchio::buildReducedModel(raw_model_, list_of_joints_to_lock_by_id, q_locked);
+ data_ = pinocchio::Data(model_);
+
+ for (int joint_id = 0; joint_id < model_.njoints; joint_id++) {
+ if (model_.names[joint_id] == "universe") {
+ continue;
+ }
+ if (not allowed_joint_types.count(model_.joints[joint_id].shortname())) {
+ RCLCPP_ERROR_STREAM(get_node()->get_logger(), "Joint type " << model_.joints[joint_id].shortname() << " is unsupported (" << model_.names[joint_id] << "), only revolute/continous like joints can be used.");
+ return CallbackReturn::ERROR;
+ }
+ }
+
+ end_effector_frame_id = model_.getFrameId(params_.end_effector_frame);
+ q = Eigen::VectorXd::Zero(model_.nv);
+ q_dot = Eigen::VectorXd::Zero(model_.nv);
+
+ twist_publisher_ = get_node()->create_publisher(
+ "current_twist", rclcpp::SystemDefaultsQoS());
+ realtime_twist_publisher_ =
+ std::make_shared>(
+ twist_publisher_);
+
+ last_publish_time_ = this->get_node()->now();
+ return CallbackReturn::SUCCESS;
+}
+
+CallbackReturn TwistBroadcaster::on_activate(
+ const rclcpp_lifecycle::State & /*previous_state*/) {
+ return CallbackReturn::SUCCESS;
+}
+
+controller_interface::CallbackReturn TwistBroadcaster::on_deactivate(
+ const rclcpp_lifecycle::State & /*previous_state*/) {
+ return CallbackReturn::SUCCESS;
+}
+
+} // namespace crisp_controllers
+#include "pluginlib/class_list_macros.hpp"
+// NOLINTNEXTLINE
+PLUGINLIB_EXPORT_CLASS(crisp_controllers::TwistBroadcaster,
+ controller_interface::ControllerInterface)
diff --git a/src/twist_broadcaster.yaml b/src/twist_broadcaster.yaml
new file mode 100644
index 0000000..a455667
--- /dev/null
+++ b/src/twist_broadcaster.yaml
@@ -0,0 +1,15 @@
+twist_broadcaster:
+ joints:
+ type: string_array
+ default_value: []
+ description: "Names of the joints"
+ end_effector_frame:
+ type: string
+ description: "Name of the end-effector frame"
+ base_frame:
+ type: string
+ description: "Name of the base frame"
+ publish_frequency:
+ type: double
+ default_value: 250.0
+ description: "Publishing frequency in Hz (0.0 means no throttling)"