Skip to content

Commit

Permalink
add URDF importer
Browse files Browse the repository at this point in the history
WIP
  • Loading branch information
baggepinnen committed Sep 18, 2024
1 parent cb4e11e commit caadbd5
Show file tree
Hide file tree
Showing 2 changed files with 242 additions and 0 deletions.
3 changes: 3 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ version = "0.2.0"
CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298"
DataInterpolations = "82cc6244-b520-54b8-b5a6-8a565e85f1d0"
FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
Graphs = "86223c79-3864-5bf0-83f7-82e725a168b6"
JuliaSimCompiler = "8391cb6b-4921-5777-4e45-fd9aab8cb88d"
LightXML = "9c8b4983-aa76-5018-a973-4c85ecc9e179"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MeshIO = "7269a6da-0436-5bbc-96c2-40638cbb6118"
MetaGraphsNext = "fa8bd995-216d-47f1-8a91-f3b68fbeb377"
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
ModelingToolkitStandardLibrary = "16a59e39-deab-5bd0-87e4-056b12336739"
Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc"
Expand Down
239 changes: 239 additions & 0 deletions src/urdf.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
using LightXML
using StaticArrays
using Rotations
using LinearAlgebra: ×, I
using Graphs
using MetaGraphsNext

@mtkmodel NullJoint begin
@components begin
frame_a = Frame()
frame_b = Frame()
end
@equations begin
connect(frame_a, frame_b)
end
end

function LightXML.find_element(x::XMLElement, n::AbstractString, ns::AbstractString...)
inner = find_element(x, n)
inner === nothing && return nothing
find_element(inner, ns...)
end

function parse_scalar(::Type{T}, e::XMLElement, name::String) where {T}
parse(T, attribute(e, name))
end

function parse_scalar(::Type{T}, e::XMLElement, name::String, default::String) where {T}
parse(T, e == nothing ? default : attribute(e, name))
end

function parse_vector(::Type{T}, e::Union{XMLElement, Nothing}, name::String, default::String) where {T}
usedefault = e == nothing || attribute(e, name) == nothing # TODO: better handling of required attributes
[parse(T, str) for str in split(usedefault ? default : attribute(e, name))]
end

function parse_color(xml_link, default)
color_elem = find_element(xml_link, "visual", "material", "color")
parse_vector(Float64, color_elem, "rgba", "1 0 0 1")
end

function parse_inertia_mat(::Type{T}, xml_inertia::XMLElement) where {T}
ixx = parse_scalar(T, xml_inertia, "ixx", "0")
ixy = parse_scalar(T, xml_inertia, "ixy", "0")
ixz = parse_scalar(T, xml_inertia, "ixz", "0")
iyy = parse_scalar(T, xml_inertia, "iyy", "0")
iyz = parse_scalar(T, xml_inertia, "iyz", "0")
izz = parse_scalar(T, xml_inertia, "izz", "0")
[ixx ixy ixz; ixy iyy iyz; ixz iyz izz]
end

function parse_pose(::Type{T}, xml_pose::Nothing) where {T}
rot = one(RotMatrix3{T})
trans = zeros(3)
rot, trans
end

function parse_pose(::Type{T}, xml_pose::XMLElement) where {T}
rpy = parse_vector(T, xml_pose, "rpy", "0 0 0")
rot = RotMatrix(RotZYX(rpy[3], rpy[2], rpy[1]))
trans = parse_vector(T, xml_pose, "xyz", "0 0 0")
rot, trans
end

function parse_joint_type(::Type{T}, xml_joint::XMLElement, name) where {T}
urdf_joint_type = attribute(xml_joint, "type")
joint_type = joint_types[urdf_joint_type]
# TODO: add Damper from <dynamics damping="0.1" />
# TODO: add color information
connections = ""
if urdf_joint_type == "revolute" || urdf_joint_type == "continuous"
axis = parse_vector(T, find_element(xml_joint, "axis"), "xyz", "1 0 0")
damping = parse_scalar(T, find_element(xml_joint, "dynamics"), "damping", "0")
if damping == "0"
components = "$name = Revolute(; n=$axis)"
else
components = """
$name = Revolute(; n=$axis, axisflange=true)
$(name)_damper = Rotational.Damper(; d=$damping)
"""
connections = """
connect($name.support, $(name)_damper.flange_a)
connect($(name)_damper.flange_b, $name.axis)
"""
end
elseif urdf_joint_type == "prismatic"
axis = parse_vector(T, find_element(xml_joint, "axis"), "xyz", "1 0 0")
components = "$name = Prismatic(; n=$axis)"
elseif urdf_joint_type == "floating"
components = "$name = FreeMotion()"
connections = "connect(world.frame_b, $name.frame_a)"
elseif urdf_joint_type == "fixed"
components = "$name = NullJoint()" # Null joint
elseif urdf_joint_type == "planar"
urdf_axis = parse_vector(T, find_element(xml_joint, "axis"), "xyz", "1 0 0")
# The URDF spec says that a planar joint allows motion in a
# plane perpendicular to the axis.
R = Rotations.rotation_between([0, 0, 1], urdf_axis)
x_axis = R * [1, 0, 0]
components = "$name = $(joint_type)(; n_x=$x_axis, n=$urdf_axis)"
else
error("joint type $(urdf_joint_type) not recognized")
end
components, connections
end

function parse_joint(::Type{T}, xml_joint::XMLElement) where {T}
name = attribute(xml_joint, "name")
parse_joint_type(T, xml_joint, name)
# position_bounds, velocity_bounds, effort_bounds = parse_joint_bounds(joint_type, xml_joint)
end

function parse_inertia(::Type{T}, xml_inertial::XMLElement) where {T}
moment = parse_inertia_mat(T, find_element(xml_inertial, "inertia"))
mass = parse_scalar(T, find_element(xml_inertial, "mass"), "value", "0")
# TODO: handle transformation of inertia
mass, moment
end

function parse_body(::Type{T}, xml_link::XMLElement) where {T}
xml_inertial = find_element(xml_link, "inertial")
mass,inertia = xml_inertial == nothing ? (0,0*I(3)) : parse_inertia(T, xml_inertial)
linkname = attribute(xml_link, "name")
R, r = parse_pose(T, find_element(xml_link, "visual", "origin"))

@show color = parse_color(xml_link, "1 0 0 1")
cylinder = find_element(xml_link, "visual", "geometry", "cylinder")
radius = if cylinder === nothing
0.1
else
parse_scalar(T, cylinder, "radius", "0.1")
end
if R != I
@warn "Ignoring rotation of link $linkname"
end
"$(Symbol(linkname)) = BodyShape(r=$(r), m=$(mass), I_11 = $(inertia[1,1]), I_22 = $(inertia[2,2]), I_33 = $(inertia[3,3]), I_21 = $(inertia[2,1]), I_31 = $(inertia[3,1]), I_32 = $(inertia[3,2]), color=$(color), radius=$(radius))"
end


function parse_urdf(filename::AbstractString; extras=false, out=nothing)
gravity = 9.81
floating::Bool = false
joint_types = default_urdf_joint_types()
root_joint_type = joint_types[floating ? "floating" : "fixed"](name=:root)

xdoc = parse_file(filename)
xroot = LightXML.root(xdoc)
@assert LightXML.name(xroot) == "robot"

xml_links = get_elements_by_tagname(xroot, "link")
xml_joints = get_elements_by_tagname(xroot, "joint")
poses = map(xml_joints) do xml_joint
pose = parse_pose(Float64, find_element(xml_joint, "origin"))
end

# create graph structure of XML elements
graph = MetaGraph(Graph(), label_type=String, vertex_data_type=eltype(xml_links), edge_data_type=eltype(xml_joints))
for (i,vertex) in enumerate(xml_links)
# add_vertex!(graph)
graph[attribute(vertex, "name")] = vertex
end
name_to_vertex = Dict(attribute(v, "name") => i for (i,v) in enumerate(xml_links))
for xml_joint in xml_joints

parent = attribute(find_element(xml_joint, "parent"), "link")
child = attribute(find_element(xml_joint, "child"), "link")
graph[parent, child] = xml_joint
end

joints_extraconnections = map(xml_joints) do l
parse_joint(Float64, l, joint_types)
end
joints = first.(joints_extraconnections)
extra_connections = filter(!isempty, last.(joints_extraconnections))

bodies = map(xml_links) do l
parse_body(Float64, l)
end

connections = map(edges(graph)) do e
src_link = label_for(graph, e.src)
dst_link = label_for(graph, e.dst)
joint = attribute(graph[src_link, dst_link], "name")
"""
connect($(src_link).frame_b, $(joint).frame_a)
connect($(joint).frame_b, $(dst_link).frame_a)
"""

end

s = if extras
"""
using ModelingToolkit, Multibody, JuliaSimCompiler, OrdinaryDiffEq, Plots
W(args...; kwargs...) = Multibody.world
"""
else
""
end
s = s * """
@mtkmodel URDFModel begin
@components begin
world = W()
$(join(bodies, "\n"))
$(join(joints, "\n"))
end
@equations begin
$(join(connections, "\n"))
$(join(extra_connections, "\n"))
end
end
"""
if extras
s = s * """
@named model = URDFModel()
model = complete(model)
ssys = structural_simplify(IRSystem(model))
prob = ODEProblem(ssys, [], (0.0, 10.0))
sol = solve(prob, FBDF())
plot(sol)
"""
end
out === nothing && return s

write(out, s)

@eval Main (using JuliaFormatter; JuliaFormatter.format_file($out, align_assignment=true, align_struct_field=true, align_conditional=true, align_pair_arrow=true))

s

end

function outer_model(f, io, name="URDFModel")
println(io, "@mtkmodel $name begin")
f(io)
println(io, "end")
end

# filename = "/home/fredrikb/.julia/dev/Multibody/test/doublependulum.urdf"

0 comments on commit caadbd5

Please sign in to comment.