Skip to content

feat: add mix bb.from_urdf URDF importer#107

Merged
jimsynz merged 4 commits into
mainfrom
feat/urdf-import
May 18, 2026
Merged

feat: add mix bb.from_urdf URDF importer#107
jimsynz merged 4 commits into
mainfrom
feat/urdf-import

Conversation

@jimsynz
Copy link
Copy Markdown
Contributor

@jimsynz jimsynz commented May 18, 2026

Closes #101.

Summary

Adds a mix bb.from_urdf Igniter task that reads a URDF XML file and generates an equivalent BB DSL module:

mix bb.from_urdf path/to/robot.urdf --module MyApp.Robot

Three pieces:

  • BB.Urdf.Parser — parses URDF XML into a plain-map representation via :xmerl_scan (no extra deps). Collects warnings for <mimic>, <safety_controller>, <transmission>, and <gazebo> blocks rather than failing the import, so partial fidelity is preferred over rejecting real-world URDFs.
  • BB.Urdf.Importer — converts parsed URDF into a quoted BB DSL module and formats it via Sourceror with ~u sigil literals for unit-bearing values. Joints are nested under their parent links in topology order; the root link is identified as the one never appearing as a <child>. Cardinal axis vectors map to BB's axis do roll/pitch end Euler form (0 1 0roll ~u(-90 degree), etc.); arbitrary vectors get rotated to Z via computed Euler angles.
  • mix bb.from_urdf — Igniter task that wraps the two, writes the module via Igniter.Project.Module.create_module, and surfaces parser warnings via Igniter.add_warning/2.

What's deliberately not in this PR

  • Mesh file management. Mesh filename attributes (including package:// URIs) are passed through verbatim. The user copies meshes and rewrites paths themselves; that felt like a separate concern from generation.
  • <mimic> / <safety_controller> / <transmission> translation. No clean BB analogue — surfaced as warnings instead.
  • Tutorial doc. 07-urdf-import.md from the issue's "Related" section. The existing 07-parameters.md makes the numbering awkward; happy to add this as a follow-up once we decide on a slot (e.g. renumber, or move to documentation/how-to/).
  • Regenerating bb_example_wx200's definition from a vendor URDF as a smoke test, per the issue's open question. Round-trip works on the test fixture (URDF → DSL → compile → URDF), but adding the example regen feels like a separate PR.

Test plan

  • mix test test/bb/urdf/parser_test.exs (8 tests)
  • mix test test/bb/urdf/importer_test.exs (10 tests, including a generated-source-compiles check)
  • mix test test/mix/tasks/bb.from_urdf_test.exs (4 Igniter tests)
  • mix test (938 tests, no regressions)
  • mix format --check-formatted
  • mix credo --strict
  • mix dialyzer (clean)
  • Manual round-trip on two_link_arm.urdf: parse → generate DSL → compile → export → same structure

jimsynz added 2 commits May 18, 2026 08:25
…101)

Adds three pieces:

- `BB.Urdf.Parser` — parses URDF XML into a plain-map representation
  via `:xmerl_scan`. Collects warnings for `<mimic>`,
  `<safety_controller>`, `<transmission>`, and `<gazebo>` blocks rather
  than failing.
- `BB.Urdf.Importer` — converts the parsed representation into a quoted
  BB DSL module and formats it via Sourceror, with `~u` sigil literals
  for unit-bearing values and joints nested under their parent links in
  topology order. Available when Sourceror is loaded (i.e. via igniter).
- `mix bb.from_urdf path/to/robot.urdf --module MyApp.Robot` — Igniter
  task that wraps the parser and importer and writes the module via
  `Igniter.Project.Module.create_module`.

Mesh files are referenced as-is (no `package://` rewriting in v1).
Tested the importer against UR5, Franka Panda, ANYmal B, and Allegro
Hand URDFs from public ROS packages. Two issues fell out:

- URDF allows many visuals to reference one named `<material>`. BB's
  DSL requires globally-unique entity names, so the generated module
  failed to compile. Keep the URDF name on the first visual that uses
  each material; strip it on later occurrences (BB's `material` entity
  auto-generates an identifier when `name` is absent).
- `BB.Dsl.Mesh.scale` defaults to the integer `1`, and the URDF
  exporter calls `:erlang.float_to_binary/2` on it, which crashes.
  Always emit `scale` as a float so the resulting module round-trips
  through `mix bb.to_urdf` without hitting that bug.

After these fixes, all four corpus URDFs parse, generate, compile, and
round-trip through the exporter with the same link/joint counts as the
inputs.
@jimsynz
Copy link
Copy Markdown
Contributor Author

jimsynz commented May 18, 2026

Tested against real-world URDFs

Ran the importer against four publicly-available URDFs to shake out edge cases:

URDF Source Links / Joints Result
Universal Robots UR5 ros-industrial 9 / 8 ✅ parse → generate → compile → round-trip
Franka Panda StanfordASL mirror 12 / 11 ✅ parse → generate → compile → round-trip
ANYmal B ANYbotics simple desc 22 / 21 ✅ parse → generate → compile → round-trip
Allegro Hand simlabrobotics 21 / 20 ✅ parse → generate → compile → round-trip

Round-trip = parse → generate DSL → compile → BB.Urdf.Exporter.export/1, with link/joint counts preserved.

Two real issues surfaced and are fixed in 1092ec7:

  • URDF lets many visuals reference one named <material>, but BB's DSL requires globally-unique entity names — the four corpus modules all failed to compile because the generated source had things like four material name(:panda_white) blocks. Fix: keep the URDF name on the first visual that uses each material; subsequent occurrences emit anonymous materials (BB auto-generates an identifier when name is absent). Regression test added with a synthetic fixture.
  • BB.Dsl.Mesh.scale defaults to the integer 1, and the URDF exporter calls :erlang.float_to_binary/2 on it, which crashes. Always emit scale as a float so the generated module round-trips. (The exporter bug is pre-existing — happy to file a separate issue if useful.)

Parser warnings observed on the corpus all look correct: <safety_controller> on every Panda joint, the <mimic> on the Panda gripper finger, <transmission> on every UR5 actuator, <gazebo> extensions on ANYmal, and Allegro's repeated material "black" referenced by name but not defined at top level for inline material references with no top-level def.

`BB.Sensor.Mimic` already implements URDF mimic semantics — same
`position * multiplier + offset` formula, even documents the URDF
equivalent in its docstring. So instead of skipping `<mimic>` with a
warning, the importer now attaches a `BB.Sensor.Mimic` sensor to the
mimicking joint pointing back at the source joint, with multiplier and
offset only emitted when they differ from URDF's defaults of 1.0 and
0.0.

Confirmed on the Franka Panda fixture (`panda_finger_joint2` mimics
`panda_finger_joint1`): the generated module now wires up the mimic
sensor automatically instead of leaving the user to add it by hand.
@jimsynz
Copy link
Copy Markdown
Contributor Author

jimsynz commented May 18, 2026

Good catch — BB.Sensor.Mimic is documented as the URDF mimic equivalent (same position * multiplier + offset formula). Wired it up in 3159a56: <mimic> joints now emit a BB.Sensor.Mimic attached to the mimicking joint instead of getting skipped with a warning.

For example, the Panda's panda_finger_joint2 now generates:

joint :panda_finger_joint2 do
  type :prismatic
  ...
  sensor :panda_finger_joint2_mimic,
         {BB.Sensor.Mimic, source: :panda_finger_joint1}

  link :panda_rightfinger
end

Multiplier and offset are omitted when they match URDF's defaults of 1.0 and 0.0. Updated tests on the existing mimic_and_transmission.urdf fixture (which has multiplier="-1") verify the non-default case.

Two failures that fell out of testing against a wider URDF corpus
(Bullet's minitaur, the so101 from onshape-to-robot, drake's acrobot):

- Many URDFs anchor the base link to the world frame with a fixed
  joint whose parent is a synthetic `world` link that's never defined.
  BB has no concept of a world frame — the topology root is the robot.
  Drop such joints in a pre-pass, leaving the child unparented so the
  root-finder picks it up. A warning records what was dropped.
- URDF has separate namespaces for link and joint names, but BB
  requires global uniqueness across both. Several real URDFs use the
  same string for a link and a joint (so101's `gripper`, minitaur's
  `motor_back_rightL_link`). Rename colliding joints with a `_joint`
  suffix (incrementing if that's also taken) and rewrite `<mimic>`
  source references to match.

With these fixes, eleven public URDFs now compile cleanly: Allegro
Hand, ANYmal B, Bullet cartpole, Drake acrobot/pendulum, KUKA iiwa,
Bullet minitaur, Franka Panda, SO-101, UR5, and WidowX (xacro-free
portions).
@jimsynz
Copy link
Copy Markdown
Contributor Author

jimsynz commented May 18, 2026

Expanded corpus

Ran against eleven public URDFs spanning industrial arms, quadrupeds, mobile robots, and simple physics models. Two new failure modes surfaced (fixed in ecc341c) before everything compiled and round-tripped:

URDF Source Links / Joints Notes
Allegro Hand simlabrobotics 21 / 20 inline material refs without top-level def → parser warnings
ANYmal B ANYbotics simple desc 22 / 21 <gazebo> extensions skipped
Cartpole Bullet 3 / 2 clean
Drake acrobot RobotLocomotion 3 / 3 <joint parent="world"> dropped via new world-anchor pass
Drake pendulum RobotLocomotion 4 / 3 clean
KUKA iiwa Bullet 8 / 7 clean
Bullet minitaur Bullet 27 / 26 hit the new joint/link name-collision rename
Franka Panda StanfordASL 12 / 11 <mimic>BB.Sensor.Mimic, <safety_controller> skipped
SO-101 TheRobotStudio 7 / 6 name-collision rename for the gripper link/joint pair
UR5 ros-industrial 9 / 8 <transmission> and <gazebo> skipped
WidowX Interbotix 8 / 7 xacro file; non-xacro portions form a valid robot

The two new pre-passes:

  • World anchor: URDFs commonly anchor the base to a synthetic world link with a fixed joint. BB has no world-frame concept — the topology root is the robot. Such joints are now dropped with a warning, leaving the child unparented so the root-finder picks it up.
  • Name collision: Several URDFs reuse a name across link and joint namespaces (so101's gripper, minitaur's motor_back_rightL_link). Colliding joints get renamed with a _joint suffix and <mimic> source references are rewritten to match.

Regression tests added with synthetic fixtures for both cases.

@jimsynz jimsynz merged commit 1fd3f18 into main May 18, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

URDF import: generate a BB DSL module from a URDF file

1 participant