Architecture
URDF is the source of truth. Geometry originates in CAD, is exported by the modeller as SDF, and is converted into URDF by a script rather than by hand.
modeller ──▶ <part>.glb + model.sdf ──▶ import script ──▶ <part>.urdf.xacro
│
assemblies (xacro) ◀────────┘
│
┌─────────────────────┴─────────────────────┐
▼ ▼
ROS: URDF direct Gazebo: merge-includes the URDF
Nothing converts formats at runtime. Both consumption paths see the URDF the build produces.
Assemblies
Developers write a xacro macro per part; assemblies instantiate them and add the
joints. Joints belong to whatever composes the parts, never to a part itself —
both fixed mounts and the continuous joints that spin propellers.
<xacro:macro name="t200_housing" params="name parent *origin">
<link name="${name}">
<inertial>…</inertial>
<visual>
<geometry>
<mesh filename="package://bluerobotics_parts/models/t200_housing/t200_housing.glb"/>
</geometry>
</visual>
<!-- BEGIN GENERATED COLLISIONS -->
<!-- END GENERATED COLLISIONS -->
</link>
<joint name="${name}_joint" type="fixed">
<parent link="${parent}"/><child link="${name}"/>
<xacro:insert_block name="origin"/>
</joint>
</xacro:macro>
Each vehicle offers a simple, complete standalone configuration and a programmatic method (xacro and yaml) for more complex ones. Simple things simple: reuse the checked-in default robot, no config. Complex things possible: the programmatic path for custom loadouts. The two are deliberately not kept in sync — the standalone configuration is conceptually the programmatic one with every option turned off, but nothing enforces that, and promising it would create a maintenance obligation with no payoff.
Why it works this way
The problem is transcription, not format. The modeller’s tooling exports SDF; the description packages consume URDF. When a person bridges that gap by retyping geometry, that hand step is the only place defects can enter. A contributed part once arrived with cylinders of radius 0.41 m on a 1.2 m hull, collision names containing spaces, geometry yawed 180°, and SDF syntax pasted into a URDF file where it could not parse — committed commented out, with “TODO: Verify I’m doing this right”. The task as posed had no obvious right answer, which is why it should not be posed to a person. None of it was detectable until the whole vehicle assembled, because there was no smaller unit to test.
SDF-first was prototyped and rejected. Parts as standalone SDF models,
composed with <include merge="true"> and converted for ROS by sdformat_urdf,
is what Gazebo’s interoperability
docs recommend, and it works —
verified end to end. It was rejected on ecosystem grounds: benchmarking robots
that ship to both ROS and Gazebo (clearpath_common 130 xacro / 0 SDF,
turtlebot4 8 / 0, Universal_Robots_ROS2_Description 8 / 0), nobody is
SDF-first. Putting a young, thinly-maintained converter on the critical path for
every robot description would make its bugs ours to find. The asset-pipeline
problem it solved is better handled by a script we own.
The structure is Clearpath’s.
clearpath_common is
the closest analogue — a family of configurable robots targeting both ROS and
Gazebo, built from a shared parts library plus a yaml-driven generator. The
addition here is the import script, which they do not need because their
geometry does not arrive from an artist exporting SDF.