Current section
Files
Jump to
Current section
Files
lib/mix/tasks/bb.from_urdf.ex
# SPDX-FileCopyrightText: 2026 James Harton## SPDX-License-Identifier: Apache-2.0if Code.ensure_loaded?(Igniter) do defmodule Mix.Tasks.Bb.FromUrdf do @shortdoc "Generate a BB robot module from a URDF file" @moduledoc """ #{@shortdoc} Reads a URDF XML file and writes a `defmodule` that uses `BB` with an equivalent topology to your project. If the target module already exists, only its `topology do ... end` block is replaced — `settings`, sensors, controllers, commands and any other hand-written content are left in place. If the existing module has no `topology` block, one is appended. ## Example ```bash mix bb.from_urdf path/to/robot.urdf --module MyApp.Robot ``` ## Options * `--module`, `-m` - The module name for the generated robot. Defaults to `{AppPrefix}.Robot`. ## URDF feature support `<mimic>` joints are emitted as a `BB.Sensor.Mimic` attached to the mimicking joint — BB's sensor implements the same `position * multiplier + offset` semantics as URDF. These URDF features have no direct BB equivalent and are skipped with a warning rather than failing the import: * `<safety_controller>` blocks * `<transmission>` blocks * `<gazebo>` extensions Mesh files are referenced as-is — `package://` URIs from ROS are not rewritten, so you may need to copy meshes into the project and adjust paths by hand. """ use Igniter.Mix.Task alias BB.Urdf.{Importer, Parser} alias Igniter.Code.{Common, Function} @impl Igniter.Mix.Task def info(_argv, _parent) do %Igniter.Mix.Task.Info{ positional: [:urdf_path], schema: [module: :string], aliases: [m: :module] } end @impl Igniter.Mix.Task def igniter(igniter) do %{urdf_path: urdf_path} = igniter.args.positional module_name = resolve_module(igniter) case Parser.parse_file(urdf_path) do {:ok, parsed} -> generate(igniter, parsed, module_name) {:error, reason} -> Igniter.add_issue(igniter, "Could not parse URDF #{urdf_path}: #{inspect(reason)}") end end defp resolve_module(igniter) do case Keyword.get(igniter.args.options, :module) do nil -> Igniter.Project.Module.module_name(igniter, "Robot") name -> Igniter.Project.Module.parse(name) end end defp generate(igniter, parsed, module_name) do with {:ok, source, warnings} <- Importer.to_source(parsed, module_name), {:ok, topology_ast, _} <- Importer.to_topology_quoted(parsed) do igniter |> find_and_update_or_create_module(module_name, source, topology_ast) |> add_warnings(warnings) else {:error, reason} -> Igniter.add_issue(igniter, "Could not generate BB module: #{inspect(reason)}") end end defp find_and_update_or_create_module(igniter, module_name, source, topology_ast) do body = strip_defmodule(source, module_name) Igniter.Project.Module.find_and_update_or_create_module( igniter, module_name, body, fn zipper -> replace_or_insert_topology(zipper, topology_ast) end ) end defp replace_or_insert_topology(zipper, topology_ast) do case Function.move_to_function_call_in_current_scope(zipper, :topology, 1) do {:ok, found} -> {:ok, Common.replace_code(found, topology_ast)} :error -> {:ok, Common.add_code(zipper, topology_ast)} end end defp strip_defmodule(source, module_name) do module_string = Macro.inspect_atom(:literal, module_name) source |> String.replace(~r/\Adefmodule\s+#{Regex.escape(module_string)}\s+do\n?/, "") |> String.replace(~r/\nend\s*\z/, "") |> String.trim_trailing() end defp add_warnings(igniter, warnings) do Enum.reduce(warnings, igniter, fn warning, acc -> Igniter.add_warning(acc, warning) end) end endelse defmodule Mix.Tasks.Bb.FromUrdf do @shortdoc "Generate a BB robot module from a URDF file" @moduledoc false use Mix.Task def run(_argv) do Mix.shell().error(""" The bb.from_urdf task requires igniter. mix deps.get mix bb.from_urdf path/to/robot.urdf --module MyApp.Robot """) exit({:shutdown, 1}) end endend