forked from pz4kybsvg/Conception
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1054 lines
40 KiB
1054 lines
40 KiB
#include "drake/multibody/parsing/detail_urdf_parser.h"
|
|
|
|
#include <limits>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <unordered_set>
|
|
#include <utility>
|
|
#include <variant>
|
|
#include <vector>
|
|
|
|
#include <Eigen/Dense>
|
|
#include <drake_vendor/tinyxml2.h>
|
|
#include <fmt/format.h>
|
|
|
|
#include "drake/common/sorted_pair.h"
|
|
#include "drake/math/rotation_matrix.h"
|
|
#include "drake/multibody/parsing/detail_make_model_name.h"
|
|
#include "drake/multibody/parsing/detail_path_utils.h"
|
|
#include "drake/multibody/parsing/detail_tinyxml.h"
|
|
#include "drake/multibody/parsing/detail_tinyxml2_diagnostic.h"
|
|
#include "drake/multibody/parsing/detail_urdf_geometry.h"
|
|
#include "drake/multibody/parsing/package_map.h"
|
|
#include "drake/multibody/plant/multibody_plant.h"
|
|
#include "drake/multibody/tree/ball_rpy_joint.h"
|
|
#include "drake/multibody/tree/fixed_offset_frame.h"
|
|
#include "drake/multibody/tree/planar_joint.h"
|
|
#include "drake/multibody/tree/prismatic_joint.h"
|
|
#include "drake/multibody/tree/revolute_joint.h"
|
|
#include "drake/multibody/tree/scoped_name.h"
|
|
#include "drake/multibody/tree/screw_joint.h"
|
|
#include "drake/multibody/tree/universal_joint.h"
|
|
#include "drake/multibody/tree/weld_joint.h"
|
|
|
|
namespace drake {
|
|
namespace multibody {
|
|
namespace internal {
|
|
|
|
using Eigen::Matrix3d;
|
|
using Eigen::Vector3d;
|
|
using Eigen::Vector4d;
|
|
using math::RigidTransformd;
|
|
using tinyxml2::XMLNode;
|
|
using tinyxml2::XMLDocument;
|
|
using tinyxml2::XMLElement;
|
|
|
|
namespace {
|
|
|
|
using JointEffortLimits = std::map<std::string, double>;
|
|
|
|
// Helper class to share infrastructure among parsing methods.
|
|
class UrdfParser {
|
|
public:
|
|
// Note that @p data_source, @p xml_doc, and @p w are aliased for the
|
|
// lifetime of this object. Their lifetimes must exceed that of the created
|
|
// object.
|
|
UrdfParser(
|
|
const DataSource* data_source,
|
|
const std::string& model_name,
|
|
const std::optional<std::string>& parent_model_name,
|
|
const std::string& root_dir,
|
|
XMLDocument* xml_doc,
|
|
const ParsingWorkspace& w)
|
|
: model_name_(model_name),
|
|
parent_model_name_(parent_model_name),
|
|
root_dir_(root_dir),
|
|
xml_doc_(xml_doc),
|
|
w_(w),
|
|
diagnostic_(&w.diagnostic, data_source) {
|
|
DRAKE_DEMAND(data_source != nullptr);
|
|
DRAKE_DEMAND(xml_doc != nullptr);
|
|
}
|
|
|
|
// @return a model instance index, if one was created during parsing.
|
|
// @throw std::exception on parse error.
|
|
// @note: see AddModelFromUrdf for a full account of diagnostics, error
|
|
// reporting, and return values.
|
|
std::optional<ModelInstanceIndex> Parse();
|
|
void ParseBushing(XMLElement* node);
|
|
void ParseFrame(XMLElement* node);
|
|
void ParseTransmission(const JointEffortLimits& joint_effort_limits,
|
|
XMLElement* node);
|
|
void ParseJoint(JointEffortLimits* joint_effort_limits, XMLElement* node);
|
|
const Body<double>* GetBodyForElement(const std::string& element_name,
|
|
const std::string& link_name);
|
|
void ParseJointDynamics(XMLElement* node, double* damping);
|
|
void ParseJointLimits(XMLElement* node, double* lower, double* upper,
|
|
double* velocity, double* acceleration, double* effort);
|
|
void ParseJointKeyParams(XMLElement* node,
|
|
std::string* name,
|
|
std::string* type,
|
|
std::string* parent_link_name,
|
|
std::string* child_link_name);
|
|
void ParseScrewJointThreadPitch(XMLElement* node, double* screw_thread_pitch);
|
|
void ParseCollisionFilterGroup(XMLElement* node);
|
|
void ParseBody(XMLElement* node, MaterialMap* materials);
|
|
SpatialInertia<double> ExtractSpatialInertiaAboutBoExpressedInB(
|
|
XMLElement* node);
|
|
|
|
// A work-alike for internal::ParseScalarAttribute() that uses the local
|
|
// diagnostic policy.
|
|
bool ParseScalarAttribute(
|
|
const tinyxml2::XMLElement* node,
|
|
const char* attribute_name, double* val) {
|
|
return internal::ParseScalarAttribute(
|
|
node, attribute_name, val,
|
|
diagnostic_.MakePolicyForNode(node));
|
|
}
|
|
void ParseMechanicalReduction(const XMLElement& node);
|
|
|
|
|
|
void Warning(const XMLNode& location, std::string message) const {
|
|
diagnostic_.Warning(location, std::move(message));
|
|
}
|
|
|
|
void Error(const XMLNode& location, std::string message) const {
|
|
diagnostic_.Error(location, std::move(message));
|
|
}
|
|
|
|
// Warn about documented URDF elements ignored by Drake.
|
|
void WarnUnsupportedElement(const XMLElement& node, const std::string& tag) {
|
|
diagnostic_.WarnUnsupportedElement(node, tag);
|
|
}
|
|
|
|
// Warn about documented URDF attributes ignored by Drake.
|
|
void WarnUnsupportedAttribute(const XMLElement& node,
|
|
const std::string& attribute) {
|
|
diagnostic_.WarnUnsupportedAttribute(node, attribute);
|
|
}
|
|
|
|
private:
|
|
const std::string model_name_;
|
|
const std::optional<std::string> parent_model_name_;
|
|
const std::string root_dir_;
|
|
XMLDocument* const xml_doc_;
|
|
const ParsingWorkspace& w_;
|
|
TinyXml2Diagnostic diagnostic_;
|
|
|
|
ModelInstanceIndex model_instance_{};
|
|
};
|
|
|
|
const char* kWorldName = "world";
|
|
|
|
SpatialInertia<double> UrdfParser::ExtractSpatialInertiaAboutBoExpressedInB(
|
|
XMLElement* node) {
|
|
RigidTransformd X_BBi;
|
|
|
|
XMLElement* origin = node->FirstChildElement("origin");
|
|
if (origin) {
|
|
X_BBi = OriginAttributesToTransform(origin);
|
|
}
|
|
|
|
double body_mass = 0;
|
|
XMLElement* mass = node->FirstChildElement("mass");
|
|
if (mass) {
|
|
ParseScalarAttribute(mass, "value", &body_mass);
|
|
}
|
|
|
|
double ixx = 0;
|
|
double ixy = 0;
|
|
double ixz = 0;
|
|
double iyy = 0;
|
|
double iyz = 0;
|
|
double izz = 0;
|
|
|
|
XMLElement* inertia = node->FirstChildElement("inertia");
|
|
if (inertia) {
|
|
ParseScalarAttribute(inertia, "ixx", &ixx);
|
|
ParseScalarAttribute(inertia, "ixy", &ixy);
|
|
ParseScalarAttribute(inertia, "ixz", &ixz);
|
|
ParseScalarAttribute(inertia, "iyy", &iyy);
|
|
ParseScalarAttribute(inertia, "iyz", &iyz);
|
|
ParseScalarAttribute(inertia, "izz", &izz);
|
|
}
|
|
|
|
const RotationalInertia<double> I_BBcm_Bi(ixx, iyy, izz, ixy, ixz, iyz);
|
|
|
|
// If this is a massless body, return a zero SpatialInertia.
|
|
if (body_mass == 0. && I_BBcm_Bi.get_moments().isZero() &&
|
|
I_BBcm_Bi.get_products().isZero()) {
|
|
return SpatialInertia<double>(body_mass, {0., 0., 0.}, {0., 0., 0});
|
|
}
|
|
// B and Bi are not necessarily aligned.
|
|
const math::RotationMatrix<double> R_BBi(X_BBi.rotation());
|
|
|
|
// Re-express in frame B as needed.
|
|
const RotationalInertia<double> I_BBcm_B = I_BBcm_Bi.ReExpress(R_BBi);
|
|
|
|
// Bi's origin is at the COM as documented in
|
|
// http://wiki.ros.org/urdf/XML/link#Elements
|
|
const Vector3d p_BoBcm_B = X_BBi.translation();
|
|
|
|
return SpatialInertia<double>::MakeFromCentralInertia(
|
|
body_mass, p_BoBcm_B, I_BBcm_B);
|
|
}
|
|
|
|
void UrdfParser::ParseBody(XMLElement* node, MaterialMap* materials) {
|
|
// TODO(rpoyner-tri): legacy undocumented tag: remove, fix?
|
|
std::string drake_ignore;
|
|
if (ParseStringAttribute(node, "drake_ignore", &drake_ignore) &&
|
|
drake_ignore == std::string("true")) {
|
|
return;
|
|
}
|
|
// Seen in the ROS urdfdom XSD Schema.
|
|
// See https://github.com/ros/urdfdom/blob/dbecca0/xsd/urdf.xsd
|
|
WarnUnsupportedAttribute(*node, "type");
|
|
|
|
std::string body_name;
|
|
if (!ParseStringAttribute(node, "name", &body_name)) {
|
|
Error(*node, "link tag is missing name attribute.");
|
|
return;
|
|
}
|
|
|
|
const RigidBody<double>* body_pointer{};
|
|
if (body_name == kWorldName) {
|
|
// TODO(SeanCurtis-TRI): We have no documentation about what our parsers
|
|
// support and not. The fact that we allow this behavior should be
|
|
// discoverable *somewhere*. But this function is in an anonymous
|
|
// namespace; where would the documentation go that supports this
|
|
// implementation?
|
|
body_pointer = &w_.plant->world_body();
|
|
if (node->FirstChildElement("inertial") != nullptr) {
|
|
Warning(*node, "A URDF file declared the \"world\" link and then"
|
|
" attempted to assign mass properties (via the <inertial> tag)."
|
|
" Only geometries, <collision> and <visual>, can be assigned to"
|
|
" the world link. The <inertial> tag is being ignored.");
|
|
}
|
|
} else {
|
|
SpatialInertia<double> M_BBo_B;
|
|
XMLElement* inertial_node = node->FirstChildElement("inertial");
|
|
if (!inertial_node) {
|
|
M_BBo_B = SpatialInertia<double>(0, Vector3d::Zero(),
|
|
UnitInertia<double>(0, 0, 0));
|
|
} else {
|
|
M_BBo_B = ExtractSpatialInertiaAboutBoExpressedInB(inertial_node);
|
|
}
|
|
|
|
// Add a rigid body to model each link.
|
|
body_pointer = &w_.plant->AddRigidBody(body_name, model_instance_, M_BBo_B);
|
|
}
|
|
|
|
if (w_.plant->geometry_source_is_registered()) {
|
|
const RigidBody<double>& body = *body_pointer;
|
|
|
|
std::unordered_set<std::string> geometry_names;
|
|
for (XMLElement* visual_node = node->FirstChildElement("visual");
|
|
visual_node;
|
|
visual_node = visual_node->NextSiblingElement("visual")) {
|
|
std::optional<geometry::GeometryInstance> geometry_instance =
|
|
ParseVisual(diagnostic_, body_name, w_.package_map, root_dir_,
|
|
visual_node, materials, &geometry_names);
|
|
if (!geometry_instance) { continue; }
|
|
// The parsing should *always* produce an IllustrationProperties
|
|
// instance, even if it is empty.
|
|
DRAKE_DEMAND(geometry_instance->illustration_properties() != nullptr);
|
|
w_.plant->RegisterVisualGeometry(
|
|
body, geometry_instance->pose(), geometry_instance->shape(),
|
|
geometry_instance->name(),
|
|
*geometry_instance->illustration_properties());
|
|
}
|
|
|
|
geometry_names.clear(); // See ParseCollision API; the names are per-role.
|
|
for (XMLElement* collision_node = node->FirstChildElement("collision");
|
|
collision_node;
|
|
collision_node = collision_node->NextSiblingElement("collision")) {
|
|
std::optional<geometry::GeometryInstance> geometry_instance =
|
|
ParseCollision(diagnostic_, body_name, w_.package_map, root_dir_,
|
|
collision_node, &geometry_names);
|
|
if (!geometry_instance) { continue; }
|
|
DRAKE_DEMAND(geometry_instance->proximity_properties() != nullptr);
|
|
w_.plant->RegisterCollisionGeometry(
|
|
body, geometry_instance->pose(), geometry_instance->shape(),
|
|
geometry_instance->name(),
|
|
std::move(*geometry_instance->mutable_proximity_properties()));
|
|
}
|
|
}
|
|
}
|
|
|
|
void UrdfParser::ParseCollisionFilterGroup(XMLElement* node) {
|
|
auto next_child_element = [](const ElementNode& data_element,
|
|
const char* element_name) {
|
|
return std::get<XMLElement*>(data_element)
|
|
->FirstChildElement(element_name);
|
|
};
|
|
auto next_sibling_element = [](const ElementNode& data_element,
|
|
const char* element_name) {
|
|
return std::get<XMLElement*>(data_element)
|
|
->NextSiblingElement(element_name);
|
|
};
|
|
auto has_attribute = [](const ElementNode& data_element,
|
|
const char* attribute_name) {
|
|
std::string attribute_value;
|
|
return ParseStringAttribute(std::get<XMLElement*>(data_element),
|
|
attribute_name, &attribute_value);
|
|
};
|
|
auto get_string_attribute = [this](const ElementNode& data_element,
|
|
const char* attribute_name) {
|
|
std::string attribute_value;
|
|
XMLElement* anode = std::get<XMLElement*>(data_element);
|
|
if (!ParseStringAttribute(anode, attribute_name, &attribute_value)) {
|
|
Error(*anode, fmt::format("The tag <{}> does not specify the required"
|
|
" attribute \"{}\".", anode->Value(),
|
|
attribute_name));
|
|
// Fall through to return empty string.
|
|
}
|
|
return attribute_value;
|
|
};
|
|
auto get_bool_attribute = [](const ElementNode& data_element,
|
|
const char* attribute_name) {
|
|
std::string attribute_value;
|
|
ParseStringAttribute(std::get<XMLElement*>(data_element),
|
|
attribute_name, &attribute_value);
|
|
return attribute_value == std::string("true") ? true : false;
|
|
};
|
|
ParseCollisionFilterGroupCommon(
|
|
diagnostic_.MakePolicyForNode(node), model_instance_, node, w_.plant,
|
|
w_.collision_resolver, next_child_element, next_sibling_element,
|
|
has_attribute, get_string_attribute, get_bool_attribute,
|
|
get_string_attribute);
|
|
}
|
|
|
|
// Parses a joint URDF specification to obtain the names of the joint, parent
|
|
// link, child link, and the joint type. An exception is thrown if any of these
|
|
// names cannot be determined.
|
|
//
|
|
// @param[in] node The XML node parsing the URDF joint description.
|
|
// @param[out] name A reference to a string where the name of the joint
|
|
// should be saved.
|
|
// @param[out] type A reference to a string where the joint type should be
|
|
// saved.
|
|
// @param[out] parent_link_name A reference to a string where the name of the
|
|
// parent link should be saved.
|
|
// @param[out] child_link_name A reference to a string where the name of the
|
|
// child link should be saved.
|
|
void UrdfParser::ParseJointKeyParams(XMLElement* node,
|
|
std::string* name,
|
|
std::string* type,
|
|
std::string* parent_link_name,
|
|
std::string* child_link_name) {
|
|
if (!ParseStringAttribute(node, "name", name)) {
|
|
Error(*node, "joint tag is missing name attribute");
|
|
return;
|
|
}
|
|
|
|
if (!ParseStringAttribute(node, "type", type)) {
|
|
Error(*node, fmt::format("joint '{}' is missing type attribute", *name));
|
|
return;
|
|
}
|
|
|
|
// Obtains the name of the joint's parent link.
|
|
XMLElement* parent_node = node->FirstChildElement("parent");
|
|
if (!parent_node) {
|
|
Error(*node, fmt::format("joint '{}' doesn't have a parent node!", *name));
|
|
return;
|
|
}
|
|
if (!ParseStringAttribute(parent_node, "link", parent_link_name)) {
|
|
Error(*parent_node, fmt::format("joint {}'s parent does not have a link"
|
|
" attribute!", *name));
|
|
return;
|
|
}
|
|
|
|
// Obtains the name of the joint's child link.
|
|
XMLElement* child_node = node->FirstChildElement("child");
|
|
if (!child_node) {
|
|
Error(*node, fmt::format("joint '{}' doesn't have a child node!", *name));
|
|
return;
|
|
}
|
|
if (!ParseStringAttribute(child_node, "link", child_link_name)) {
|
|
Error(*child_node, fmt::format("joint {}'s child does not have a link"
|
|
" attribute!", *name));
|
|
return;
|
|
}
|
|
}
|
|
|
|
void UrdfParser::ParseJointLimits(
|
|
XMLElement* node, double* lower, double* upper,
|
|
double* velocity, double* acceleration, double* effort) {
|
|
*lower = -std::numeric_limits<double>::infinity();
|
|
*upper = std::numeric_limits<double>::infinity();
|
|
*velocity = std::numeric_limits<double>::infinity();
|
|
*acceleration = std::numeric_limits<double>::infinity();
|
|
*effort = std::numeric_limits<double>::infinity();
|
|
|
|
XMLElement* limit_node = node->FirstChildElement("limit");
|
|
if (limit_node) {
|
|
ParseScalarAttribute(limit_node, "lower", lower);
|
|
ParseScalarAttribute(limit_node, "upper", upper);
|
|
ParseScalarAttribute(limit_node, "velocity", velocity);
|
|
ParseScalarAttribute(limit_node, "drake:acceleration", acceleration);
|
|
ParseScalarAttribute(limit_node, "effort", effort);
|
|
}
|
|
}
|
|
|
|
void UrdfParser::ParseJointDynamics(XMLElement* node, double* damping) {
|
|
*damping = 0.0;
|
|
double coulomb_friction = 0.0;
|
|
double coulomb_window = std::numeric_limits<double>::epsilon();
|
|
|
|
XMLElement* dynamics_node = node->FirstChildElement("dynamics");
|
|
if (dynamics_node) {
|
|
ParseScalarAttribute(dynamics_node, "damping", damping);
|
|
if (ParseScalarAttribute(dynamics_node, "friction", &coulomb_friction) &&
|
|
coulomb_friction != 0.0) {
|
|
Warning(*dynamics_node, "A joint has specified a non-zero value for the"
|
|
" 'friction' attribute of a joint/dynamics tag. MultibodyPlant"
|
|
" does not currently support non-zero joint friction.");
|
|
}
|
|
if (ParseScalarAttribute(
|
|
dynamics_node, "coulomb_window", &coulomb_window)) {
|
|
Warning(*dynamics_node, "A joint has specified a value for the"
|
|
" 'coulomb_window' attribute of a <joint> tag. Drake no longer"
|
|
" makes use of that attribute; all instances will be ignored.");
|
|
}
|
|
}
|
|
}
|
|
|
|
void UrdfParser::ParseScrewJointThreadPitch(XMLElement* node,
|
|
double* screw_thread_pitch) {
|
|
// Always set a value for the output-only argument, even if parsing fails.
|
|
*screw_thread_pitch = 0.0;
|
|
XMLElement* screw_thread_pitch_node =
|
|
node->FirstChildElement("drake:screw_thread_pitch");
|
|
if (screw_thread_pitch_node) {
|
|
if (!ParseScalarAttribute(screw_thread_pitch_node, "value",
|
|
screw_thread_pitch)) {
|
|
Error(*screw_thread_pitch_node, "A screw joint has a"
|
|
" <drake:screw_thread_pitch> tag that is missing the 'value'"
|
|
" attribute.");
|
|
return;
|
|
}
|
|
} else {
|
|
Error(*node, "A screw joint is missing the <drake:screw_thread_pitch>"
|
|
" tag.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
const Body<double>* UrdfParser::GetBodyForElement(
|
|
const std::string& element_name,
|
|
const std::string& link_name) {
|
|
auto plant = w_.plant;
|
|
if (link_name == kWorldName) {
|
|
return &plant->world_body();
|
|
}
|
|
|
|
if (!plant->HasBodyNamed(link_name, model_instance_)) {
|
|
Error(*xml_doc_, fmt::format("Could not find link named '{}' with model"
|
|
" instance ID {} for element '{}'.", link_name,
|
|
model_instance_, element_name));
|
|
return nullptr;
|
|
}
|
|
return &plant->GetBodyByName(link_name, model_instance_);
|
|
}
|
|
|
|
void UrdfParser::ParseJoint(
|
|
JointEffortLimits* joint_effort_limits,
|
|
XMLElement* node) {
|
|
// TODO(rpoyner-tri): legacy undocumented tag: remove, fix?
|
|
std::string drake_ignore;
|
|
if (ParseStringAttribute(node, "drake_ignore", &drake_ignore) &&
|
|
drake_ignore == std::string("true")) {
|
|
return;
|
|
}
|
|
WarnUnsupportedElement(*node, "calibration");
|
|
WarnUnsupportedElement(*node, "safety_controller");
|
|
|
|
// Parses the parent and child link names.
|
|
std::string name, type, parent_name, child_name;
|
|
ParseJointKeyParams(node, &name, &type, &parent_name, &child_name);
|
|
if (name.empty() || type.empty() ||
|
|
parent_name.empty() || child_name.empty()) {
|
|
return;
|
|
}
|
|
|
|
const Body<double>* parent_body = GetBodyForElement(
|
|
name, parent_name);
|
|
if (parent_body == nullptr) { return; }
|
|
const Body<double>* child_body = GetBodyForElement(
|
|
name, child_name);
|
|
if (child_body == nullptr) { return; }
|
|
|
|
RigidTransformd X_PJ;
|
|
XMLElement* origin = node->FirstChildElement("origin");
|
|
if (origin) {
|
|
X_PJ = OriginAttributesToTransform(origin);
|
|
}
|
|
|
|
Vector3d axis(1, 0, 0);
|
|
XMLElement* axis_node = node->FirstChildElement("axis");
|
|
if (axis_node && type.compare("fixed") != 0 &&
|
|
type.compare("floating") != 0 && type.compare("ball") != 0) {
|
|
ParseVectorAttribute(axis_node, "xyz", &axis);
|
|
if (axis.norm() < 1e-8) {
|
|
Error(*axis_node, fmt::format("Joint '{}' axis is zero. Don't do that.",
|
|
name));
|
|
return;
|
|
}
|
|
axis.normalize();
|
|
}
|
|
|
|
// Joint properties -- these are only used by some joint types.
|
|
|
|
// Dynamic properties
|
|
double damping = 0;
|
|
|
|
// Limits
|
|
double upper = std::numeric_limits<double>::infinity();
|
|
double lower = -std::numeric_limits<double>::infinity();
|
|
double velocity = std::numeric_limits<double>::infinity();
|
|
double acceleration = std::numeric_limits<double>::infinity();
|
|
|
|
// In MultibodyPlant, the effort limit is a property of the actuator, which
|
|
// isn't created until the transmission element is parsed. Stash a value
|
|
// for all joints when parsing the joint element so that we can look it up
|
|
// later if/when an actuator is created.
|
|
double effort = std::numeric_limits<double>::infinity();
|
|
|
|
auto throw_on_custom_joint =
|
|
[node, name, type, this](bool want_custom_joint) {
|
|
const std::string node_name(node->Name());
|
|
const bool is_custom_joint = node_name == "drake:joint";
|
|
if (want_custom_joint && !is_custom_joint) {
|
|
Error(*node, fmt::format("Joint {} of type {} is a custom joint type, and"
|
|
" should be a <drake:joint>", name, type));
|
|
return;
|
|
} else if (!want_custom_joint && is_custom_joint) {
|
|
Error(*node, fmt::format("Joint {} of type {} is a standard joint type,"
|
|
" and should be a <joint>", name, type));
|
|
return;
|
|
}
|
|
};
|
|
|
|
auto plant = w_.plant;
|
|
std::optional<JointIndex> index{};
|
|
if (type.compare("revolute") == 0 || type.compare("continuous") == 0) {
|
|
throw_on_custom_joint(false);
|
|
ParseJointLimits(node, &lower, &upper, &velocity, &acceleration, &effort);
|
|
ParseJointDynamics(node, &damping);
|
|
index = plant->AddJoint<RevoluteJoint>(
|
|
name, *parent_body, X_PJ,
|
|
*child_body, std::nullopt, axis, lower, upper, damping).index();
|
|
Joint<double>& joint = plant->get_mutable_joint(*index);
|
|
joint.set_velocity_limits(Vector1d(-velocity), Vector1d(velocity));
|
|
joint.set_acceleration_limits(
|
|
Vector1d(-acceleration), Vector1d(acceleration));
|
|
} else if (type.compare("fixed") == 0) {
|
|
throw_on_custom_joint(false);
|
|
index = plant->AddJoint<WeldJoint>(
|
|
name, *parent_body, X_PJ, *child_body, std::nullopt,
|
|
RigidTransformd::Identity()).index();
|
|
} else if (type.compare("prismatic") == 0) {
|
|
throw_on_custom_joint(false);
|
|
ParseJointLimits(node, &lower, &upper, &velocity, &acceleration, &effort);
|
|
ParseJointDynamics(node, &damping);
|
|
index = plant->AddJoint<PrismaticJoint>(
|
|
name, *parent_body, X_PJ, *child_body, std::nullopt, axis, lower,
|
|
upper, damping).index();
|
|
Joint<double>& joint = plant->get_mutable_joint(*index);
|
|
joint.set_velocity_limits(Vector1d(-velocity), Vector1d(velocity));
|
|
joint.set_acceleration_limits(
|
|
Vector1d(-acceleration), Vector1d(acceleration));
|
|
} else if (type.compare("floating") == 0) {
|
|
throw_on_custom_joint(false);
|
|
Warning(*node, fmt::format("Joint '{}' specified as type floating which is"
|
|
" not supported by MultibodyPlant. Leaving '{}'"
|
|
" as a free body.", name, child_name));
|
|
} else if (type.compare("ball") == 0) {
|
|
throw_on_custom_joint(true);
|
|
ParseJointDynamics(node, &damping);
|
|
index = plant->AddJoint<BallRpyJoint>(
|
|
name, *parent_body, X_PJ, *child_body, std::nullopt, damping).index();
|
|
} else if (type.compare("planar") == 0) {
|
|
// Permit both the standard 'joint' and custom 'drake:joint' spellings
|
|
// here. The standard spelling was actually always correct, but Drake only
|
|
// supported the custom spelling for quite some time, and some model files
|
|
// are likely spelled that way. See #18730.
|
|
Vector3d damping_vec(0, 0, 0);
|
|
XMLElement* dynamics_node = node->FirstChildElement("dynamics");
|
|
if (dynamics_node) {
|
|
ParseVectorAttribute(dynamics_node, "damping", &damping_vec);
|
|
}
|
|
index = plant->AddJoint<PlanarJoint>(
|
|
name, *parent_body, X_PJ, *child_body, std::nullopt, damping_vec).index();
|
|
} else if (type.compare("screw") == 0) {
|
|
throw_on_custom_joint(true);
|
|
ParseJointDynamics(node, &damping);
|
|
double screw_thread_pitch;
|
|
ParseScrewJointThreadPitch(node, &screw_thread_pitch);
|
|
index = plant->AddJoint<ScrewJoint>(
|
|
name, *parent_body, X_PJ, *child_body, std::nullopt, axis,
|
|
screw_thread_pitch, damping).index();
|
|
} else if (type.compare("universal") == 0) {
|
|
throw_on_custom_joint(true);
|
|
ParseJointDynamics(node, &damping);
|
|
index = plant->AddJoint<UniversalJoint>(
|
|
name, *parent_body, X_PJ, *child_body, std::nullopt, damping).index();
|
|
} else {
|
|
Error(*node, fmt::format("Joint '{}' has unrecognized type: '{}'",
|
|
name, type));
|
|
return;
|
|
}
|
|
|
|
joint_effort_limits->emplace(name, effort);
|
|
|
|
XMLElement* mimic_node = node->FirstChildElement("mimic");
|
|
if (mimic_node) {
|
|
if (!plant->is_discrete() ||
|
|
plant->get_discrete_contact_solver() != DiscreteContactSolver::kSap) {
|
|
Warning(
|
|
*mimic_node,
|
|
fmt::format("Joint '{}' specifies a mimic element that will be "
|
|
"ignored. Mimic elements are currently only supported by "
|
|
"MultibodyPlant with a discrete time step and using "
|
|
"DiscreteContactSolver::kSap.",
|
|
name));
|
|
} else {
|
|
std::string joint_to_mimic;
|
|
double gear_ratio{1.0};
|
|
double offset{0.0};
|
|
if (!ParseStringAttribute(mimic_node, "joint", &joint_to_mimic)) {
|
|
Error(*mimic_node,
|
|
fmt::format("Joint '{}' mimic element is missing the "
|
|
"required 'joint' attribute.",
|
|
name));
|
|
return;
|
|
}
|
|
if (!plant->HasJointNamed(joint_to_mimic, model_instance_)) {
|
|
Error(*mimic_node,
|
|
fmt::format("Joint '{}' mimic element specifies joint '{}' which"
|
|
" does not exist.",
|
|
name, joint_to_mimic));
|
|
return;
|
|
}
|
|
ParseScalarAttribute(mimic_node, "multiplier", &gear_ratio);
|
|
ParseScalarAttribute(mimic_node, "offset", &offset);
|
|
|
|
if (!index) {
|
|
// This can currently happen if we have a "floating" joint, which does
|
|
// not produce the actual QuaternionFloatingJoint above.
|
|
Warning(*mimic_node,
|
|
fmt::format("Drake only supports the mimic element for "
|
|
"single-dof joints. The mimic element in joint "
|
|
"'{}' will be ignored.",
|
|
name));
|
|
} else {
|
|
const Joint<double>& joint0 = plant->get_joint(*index);
|
|
const Joint<double>& joint1 =
|
|
plant->GetJointByName(joint_to_mimic, model_instance_);
|
|
if (joint1.num_velocities() != joint0.num_velocities()) {
|
|
Error(*mimic_node,
|
|
fmt::format("Joint '{}' which has {} DOF cannot mimic "
|
|
"joint '{}' which has {} DOF.",
|
|
name, joint0.num_velocities(), joint_to_mimic,
|
|
joint1.num_velocities()));
|
|
return;
|
|
}
|
|
if (joint0.num_velocities() != 1) {
|
|
// The URDF documentation is ambiguous as to whether multi-dof joints
|
|
// are supported by the mimic tag. So we only raise a warning, not an
|
|
// error.
|
|
Warning(*mimic_node,
|
|
fmt::format("Drake only supports the mimic element for "
|
|
"single-dof joints. The joint '{}' (with {} "
|
|
"dofs) is attempting to mimic joint '{}' (with "
|
|
"{} dofs). The mimic element will be ignored.",
|
|
name, joint0.num_velocities(), joint_to_mimic,
|
|
joint1.num_velocities()));
|
|
} else {
|
|
plant->AddCouplerConstraint(joint0, joint1, gear_ratio, offset);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void UrdfParser::ParseMechanicalReduction(const XMLElement& node) {
|
|
const XMLElement* child = node.FirstChildElement("mechanicalReduction");
|
|
if (!child) { return; }
|
|
const char* text = child->GetText();
|
|
if (!text) { return; }
|
|
std::vector<double> values = ConvertToVector<double>(text);
|
|
if (values.size() == 1 && values[0] == 1) { return; }
|
|
Warning(*child, fmt::format(
|
|
"A '{}' element contains a mechanicalReduction element with a"
|
|
" value '{}' other than the default of 1. MultibodyPlant does"
|
|
" not currently support non-default mechanical reductions.",
|
|
node.Name(), text));
|
|
}
|
|
|
|
void UrdfParser::ParseTransmission(
|
|
const JointEffortLimits& joint_effort_limits,
|
|
XMLElement* node) {
|
|
WarnUnsupportedElement(*node, "leftActuator");
|
|
WarnUnsupportedElement(*node, "rightActuator");
|
|
WarnUnsupportedElement(*node, "flexJoint");
|
|
WarnUnsupportedElement(*node, "rollJoint");
|
|
WarnUnsupportedElement(*node, "gap_joint");
|
|
WarnUnsupportedElement(*node, "passive_joint");
|
|
WarnUnsupportedElement(*node, "use_simulated_gripper_joint");
|
|
ParseMechanicalReduction(*node);
|
|
|
|
// Determines the transmission type.
|
|
std::string type;
|
|
XMLElement* type_node = node->FirstChildElement("type");
|
|
if (type_node) {
|
|
type = type_node->GetText();
|
|
} else {
|
|
// Old URDF format, kept for convenience
|
|
if (!ParseStringAttribute(node, "type", &type)) {
|
|
Error(*node, "Transmission element is missing a type.");
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Checks if the transmission type is not SimpleTransmission. If it is not,
|
|
// print a warning and then abort this method call since only simple
|
|
// transmissions are supported at this time.
|
|
if (type.find("SimpleTransmission") == std::string::npos) {
|
|
Warning(*node, "A <transmission> has a type that isn't"
|
|
" 'SimpleTransmission'. Drake only supports 'SimpleTransmission';"
|
|
" all other transmission types will be ignored.");
|
|
return;
|
|
}
|
|
|
|
// Determines the actuator's name.
|
|
XMLElement* actuator_node = node->FirstChildElement("actuator");
|
|
if (!actuator_node) {
|
|
Error(*node, "Transmission is missing an actuator element.");
|
|
return;
|
|
}
|
|
ParseMechanicalReduction(*actuator_node);
|
|
// `actuator/hardwareInterface` child tags are silently ignored.
|
|
|
|
std::string actuator_name;
|
|
if (!ParseStringAttribute(actuator_node, "name", &actuator_name)) {
|
|
Error(*actuator_node, "Transmission is missing an actuator name.");
|
|
return;
|
|
}
|
|
|
|
// Determines the name of the joint to which the actuator is attached.
|
|
XMLElement* joint_node = node->FirstChildElement("joint");
|
|
if (!joint_node) {
|
|
Error(*node, "Transmission is missing a joint element.");
|
|
return;
|
|
}
|
|
// `joint/hardwareInterface` child tags are silently ignored.
|
|
|
|
|
|
std::string joint_name;
|
|
if (!ParseStringAttribute(joint_node, "name", &joint_name)) {
|
|
Error(*joint_node, "Transmission is missing a joint name.");
|
|
return;
|
|
}
|
|
|
|
auto plant = w_.plant;
|
|
if (!plant->HasJointNamed(joint_name, model_instance_)) {
|
|
Error(*joint_node, fmt::format("Transmission specifies joint '{}' which"
|
|
" does not exist.", joint_name));
|
|
return;
|
|
}
|
|
const Joint<double>& joint = plant->GetJointByName(
|
|
joint_name, model_instance_);
|
|
|
|
// Checks if the actuator is attached to a fixed joint. If so, abort this
|
|
// method call.
|
|
if (joint.num_positions() == 0) {
|
|
Warning(*joint_node, fmt::format("Skipping transmission since it's attached"
|
|
" to a fixed joint \"{}\".", joint_name));
|
|
return;
|
|
}
|
|
|
|
const auto effort_iter = joint_effort_limits.find(joint_name);
|
|
DRAKE_DEMAND(effort_iter != joint_effort_limits.end());
|
|
if (effort_iter->second < 0) {
|
|
Error(*joint_node, fmt::format("Transmission specifies joint '{}' which has"
|
|
" a negative effort limit.", joint_name));
|
|
return;
|
|
}
|
|
|
|
if (effort_iter->second <= 0) {
|
|
Warning(*joint_node, fmt::format("Skipping transmission since it's attached"
|
|
" to joint \"{}\" which has a zero effort"
|
|
" limit {}.", joint_name,
|
|
effort_iter->second));
|
|
return;
|
|
}
|
|
|
|
const JointActuator<double>& actuator =
|
|
plant->AddJointActuator(actuator_name, joint, effort_iter->second);
|
|
|
|
// Parse and add the optional drake:rotor_inertia parameter.
|
|
XMLElement* rotor_inertia_node =
|
|
actuator_node->FirstChildElement("drake:rotor_inertia");
|
|
if (rotor_inertia_node) {
|
|
double rotor_inertia;
|
|
if (!ParseScalarAttribute(rotor_inertia_node, "value", &rotor_inertia)) {
|
|
Error(*rotor_inertia_node,
|
|
fmt::format("joint actuator {}'s drake:rotor_inertia does not have"
|
|
" a \"value\" attribute!", actuator_name));
|
|
return;
|
|
}
|
|
plant->get_mutable_joint_actuator(actuator.index())
|
|
.set_default_rotor_inertia(rotor_inertia);
|
|
}
|
|
|
|
// Parse and add the optional drake:gear_ratio parameter.
|
|
XMLElement* gear_ratio_node =
|
|
actuator_node->FirstChildElement("drake:gear_ratio");
|
|
if (gear_ratio_node) {
|
|
double gear_ratio;
|
|
if (!ParseScalarAttribute(gear_ratio_node, "value", &gear_ratio)) {
|
|
Error(*gear_ratio_node,
|
|
fmt::format("joint actuator {}'s drake:gear_ratio does not have a"
|
|
" \"value\" attribute!", actuator_name));
|
|
return;
|
|
}
|
|
plant->get_mutable_joint_actuator(actuator.index())
|
|
.set_default_gear_ratio(gear_ratio);
|
|
}
|
|
}
|
|
|
|
void UrdfParser::ParseFrame(XMLElement* node) {
|
|
std::string name;
|
|
if (!ParseStringAttribute(node, "name", &name)) {
|
|
Error(*node, "parsing frame name.");
|
|
return;
|
|
}
|
|
|
|
std::string body_name;
|
|
if (!ParseStringAttribute(node, "link", &body_name)) {
|
|
Error(*node, fmt::format("missing link name for frame {}.", name));
|
|
return;
|
|
}
|
|
|
|
const Body<double>* body = GetBodyForElement(
|
|
name, body_name);
|
|
if (body == nullptr) { return; }
|
|
|
|
RigidTransformd X_BF = OriginAttributesToTransform(node);
|
|
w_.plant->AddFrame(std::make_unique<FixedOffsetFrame<double>>(
|
|
name, body->body_frame(), X_BF));
|
|
}
|
|
|
|
void UrdfParser::ParseBushing(XMLElement* node) {
|
|
// Functor to read a child element with a vector valued `value` attribute.
|
|
// Returns a zero vector if unable to find the tag or if the value attribute
|
|
// is improperly formed.
|
|
auto read_vector = [node, this](const char* element_name) -> Eigen::Vector3d {
|
|
const XMLElement* value_node = node->FirstChildElement(element_name);
|
|
if (value_node != nullptr) {
|
|
Eigen::Vector3d value;
|
|
if (ParseVectorAttribute(value_node, "value", &value)) {
|
|
return value;
|
|
} else {
|
|
Error(*node, fmt::format("Unable to read the 'value' attribute for the"
|
|
" <{}> tag", element_name));
|
|
return Eigen::Vector3d::Zero();
|
|
}
|
|
} else {
|
|
Error(*node, fmt::format("Unable to find the <{}> tag", element_name));
|
|
return Eigen::Vector3d::Zero();
|
|
}
|
|
};
|
|
|
|
// Functor to read a child element with a string-valued `name` attribute.
|
|
// Returns nullptr if unable to find the tag, if the name attribute is
|
|
// improperly formed, or if it does not refer to a frame already in the
|
|
// model.
|
|
auto read_frame = [node, this](const char* element_name)
|
|
-> const Frame<double>* {
|
|
XMLElement* value_node = node->FirstChildElement(element_name);
|
|
|
|
if (value_node != nullptr) {
|
|
std::string frame_name;
|
|
auto plant = w_.plant;
|
|
if (ParseStringAttribute(value_node, "name", &frame_name)) {
|
|
if (!plant->HasFrameNamed(frame_name, model_instance_)) {
|
|
Error(*value_node, fmt::format("Frame: {} specified for <{}> does not"
|
|
" exist in the model.", frame_name,
|
|
element_name));
|
|
return {};
|
|
}
|
|
return &plant->GetFrameByName(frame_name, model_instance_);
|
|
} else {
|
|
Error(*value_node, fmt::format("Unable to read the 'name' attribute for"
|
|
" the <{}> tag", element_name));
|
|
return {};
|
|
}
|
|
|
|
} else {
|
|
Error(*node, fmt::format("Unable to find the <{}> tag", element_name));
|
|
return {};
|
|
}
|
|
};
|
|
|
|
ParseLinearBushingRollPitchYaw(read_vector, read_frame, w_.plant);
|
|
}
|
|
|
|
std::optional<ModelInstanceIndex> UrdfParser::Parse() {
|
|
XMLElement* node = xml_doc_->FirstChildElement("robot");
|
|
if (!node) {
|
|
Error(*xml_doc_, "URDF does not contain a robot tag.");
|
|
return {};
|
|
}
|
|
// See https://github.com/ros/urdfdom/blob/dbecca0/urdf_parser/src/model.cpp#L124-L131
|
|
WarnUnsupportedAttribute(*node, "version");
|
|
// <gazebo> child tags are silently ignored.
|
|
|
|
std::string model_name = model_name_;
|
|
if (model_name.empty() && !ParseStringAttribute(node, "name", &model_name)) {
|
|
Error(*node, "Your robot must have a name attribute or a model name "
|
|
"must be specified.");
|
|
return {};
|
|
}
|
|
|
|
model_name = MakeModelName(model_name, parent_model_name_, w_);
|
|
model_instance_ = w_.plant->AddModelInstance(model_name);
|
|
|
|
// Parses the model's material elements. Throws an exception if there's a
|
|
// material name clash regardless of whether the associated RGBA values are
|
|
// the same.
|
|
MaterialMap materials;
|
|
for (XMLElement* material_node = node->FirstChildElement("material");
|
|
material_node;
|
|
material_node = material_node->NextSiblingElement("material")) {
|
|
ParseMaterial(diagnostic_, material_node, true /* name_required */,
|
|
w_.package_map, root_dir_, &materials);
|
|
}
|
|
|
|
// Parses the model's link elements.
|
|
for (XMLElement* link_node = node->FirstChildElement("link");
|
|
link_node;
|
|
link_node = link_node->NextSiblingElement("link")) {
|
|
ParseBody(link_node, &materials);
|
|
}
|
|
|
|
// Parses the collision filter groups only if the scene graph is registered.
|
|
if (w_.plant->geometry_source_is_registered()) {
|
|
ParseCollisionFilterGroup(node);
|
|
}
|
|
|
|
// Joint effort limits are stored with joints, but used when creating the
|
|
// actuator (which is done when parsing the transmission).
|
|
JointEffortLimits joint_effort_limits;
|
|
|
|
// Parses the model's joint elements. While it may not be strictly required
|
|
// to be true in MultibodyPlant, generally the JointIndex for any given
|
|
// joint follows the declaration order in the model (and users probably
|
|
// should avoid caring about the ordering of JointIndex), we still parse the
|
|
// joints in model order regardless of the element type so that the results
|
|
// are consistent with an SDF version of the same model.
|
|
for (XMLElement* joint_node = node->FirstChildElement(); joint_node;
|
|
joint_node = joint_node->NextSiblingElement()) {
|
|
const std::string node_name(joint_node->Name());
|
|
if (node_name == "joint" || node_name == "drake:joint") {
|
|
ParseJoint(&joint_effort_limits, joint_node);
|
|
}
|
|
}
|
|
|
|
// Parses the model's transmission elements.
|
|
for (XMLElement* transmission_node = node->FirstChildElement("transmission");
|
|
transmission_node;
|
|
transmission_node =
|
|
transmission_node->NextSiblingElement("transmission")) {
|
|
ParseTransmission(joint_effort_limits, transmission_node);
|
|
}
|
|
|
|
if (node->FirstChildElement("loop_joint")) {
|
|
Error(*node, "loop joints are not supported in MultibodyPlant");
|
|
return model_instance_;
|
|
}
|
|
|
|
// Parses the model's Drake frame elements.
|
|
for (XMLElement* frame_node = node->FirstChildElement("frame"); frame_node;
|
|
frame_node = frame_node->NextSiblingElement("frame")) {
|
|
ParseFrame(frame_node);
|
|
}
|
|
|
|
// Parses the model's custom Drake bushing tags.
|
|
for (XMLElement* bushing_node =
|
|
node->FirstChildElement("drake:linear_bushing_rpy");
|
|
bushing_node; bushing_node = bushing_node->NextSiblingElement(
|
|
"drake:linear_bushing_rpy")) {
|
|
ParseBushing(bushing_node);
|
|
}
|
|
|
|
return model_instance_;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
std::optional<ModelInstanceIndex> AddModelFromUrdf(
|
|
const DataSource& data_source,
|
|
const std::string& model_name_in,
|
|
const std::optional<std::string>& parent_model_name,
|
|
const ParsingWorkspace& workspace) {
|
|
MultibodyPlant<double>* plant = workspace.plant;
|
|
DRAKE_THROW_UNLESS(plant != nullptr);
|
|
DRAKE_THROW_UNLESS(!plant->is_finalized());
|
|
TinyXml2Diagnostic diag(&workspace.diagnostic, &data_source);
|
|
|
|
// Opens the URDF file and feeds it into the XML parser.
|
|
XMLDocument xml_doc;
|
|
if (data_source.IsFilename()) {
|
|
xml_doc.LoadFile(data_source.filename().c_str());
|
|
if (xml_doc.ErrorID()) {
|
|
diag.Error(xml_doc, fmt::format("Failed to parse XML file: {}",
|
|
xml_doc.ErrorName()));
|
|
return std::nullopt;
|
|
}
|
|
} else {
|
|
xml_doc.Parse(data_source.contents().c_str());
|
|
if (xml_doc.ErrorID()) {
|
|
diag.Error(xml_doc, fmt::format("Failed to parse XML string: {}",
|
|
xml_doc.ErrorName()));
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
|
|
UrdfParser parser(&data_source, model_name_in, parent_model_name,
|
|
data_source.GetRootDir(), &xml_doc, workspace);
|
|
return parser.Parse();
|
|
}
|
|
|
|
UrdfParserWrapper::UrdfParserWrapper() {}
|
|
|
|
UrdfParserWrapper::~UrdfParserWrapper() {}
|
|
|
|
std::optional<ModelInstanceIndex> UrdfParserWrapper::AddModel(
|
|
const DataSource& data_source, const std::string& model_name,
|
|
const std::optional<std::string>& parent_model_name,
|
|
const ParsingWorkspace& workspace) {
|
|
return AddModelFromUrdf(data_source, model_name, parent_model_name,
|
|
workspace);
|
|
}
|
|
|
|
std::vector<ModelInstanceIndex> UrdfParserWrapper::AddAllModels(
|
|
const DataSource& data_source,
|
|
const std::optional<std::string>& parent_model_name,
|
|
const ParsingWorkspace& workspace) {
|
|
auto result = AddModel(data_source, {}, parent_model_name, workspace);
|
|
if (result.has_value()) {
|
|
return {*result};
|
|
}
|
|
return {};
|
|
}
|
|
|
|
} // namespace internal
|
|
} // namespace multibody
|
|
} // namespace drake
|