Metadata-Version: 2.5
Name: nova2urdf
Version: 0.3.1
Summary: Export robot models from Wandelbots Nova to URDF
Project-URL: Homepage, https://www.wandelbots.com/
Author: Wandelbots GmbH
Author-email: Stefan Wagner <stefan.wagner@wandelbots.com>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: nova,robotics,ros,urdf,wandelbots
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.11
Requires-Dist: dracopy>=2.0
Requires-Dist: httpx>=0.27
Requires-Dist: numpy>=1.24
Requires-Dist: pygltflib>=1.16
Requires-Dist: scipy>=1.10
Requires-Dist: trimesh>=4.0
Provides-Extra: dev
Requires-Dist: basedpyright>=1.31.7; extra == 'dev'
Requires-Dist: poethepoet; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: radon; extra == 'dev'
Requires-Dist: ruff==0.15.4; extra == 'dev'
Requires-Dist: skylos==3.4.3; extra == 'dev'
Requires-Dist: ty; extra == 'dev'
Requires-Dist: usd-core>=24.11; extra == 'dev'
Provides-Extra: usd
Requires-Dist: usd-core>=24.11; extra == 'usd'
Description-Content-Type: text/markdown

# nova2urdf

Export the robots in a [Wandelbots NOVA](https://www.wandelbots.com/) cell to URDF, so they
can be loaded in RViz, rerun, Isaac Sim, MuJoCo, PyBullet or any other URDF tool.

You point it at a NOVA instance and get a URDF package: the kinematic chain, visual
meshes, convex collision hulls and joint limits for each robot. It exports either:

- **a controller you configured**, with its own TCP, joint limits and tool collision
  shapes, or
- **any robot in the NOVA catalogue**, including all of them at once.

## Requirements

- Python 3.11 or newer
- Network access to a NOVA instance, and an access token if the instance requires one

## Installation

```bash
pip install nova2urdf
```

To take visual materials from NOVA's USD model, so they look the way Isaac Sim draws
them, install the `usd` extra:

```bash
pip install "nova2urdf[usd]"
```

## Quickstart

```bash
export NOVA_API_URL=http://<your-nova-host>

nova2urdf list                          # robot models the instance offers
nova2urdf convert ur5e                  # one catalogue model
nova2urdf export-controller my-robot    # a controller from your cell
```

Each export lands in `robot_exports/<name>/`. Open the `.urdf` file in any URDF viewer,
or check it with `nova2urdf validate robot_exports/`.

## Command-line usage

Every command needs the NOVA instance, through `--nova-url` or the `NOVA_API_URL`
environment variable (see [Configuration](#configuration)).

### Export a controller from your cell

Use this when a controller is already configured in NOVA. It never creates or deletes
anything.

```bash
# tool0 sits at the flange
nova2urdf export-controller my-robot

# tool0 sits at a TCP you defined
nova2urdf export-controller my-robot --tcp Gripper

# lengths in millimetres instead of metres
nova2urdf export-controller my-robot --units mm
```

The export takes these from the controller:

- its **joint limits**, position and velocity;
- **every TCP** it defines, each as a frame `tcp_<id>` on the flange, so you can use any
  of them without exporting again. `--tcp` also puts `tool0` at the TCP you name;
- the **tool collision shapes** from the collision store, both user-defined and from
  the safety controller. They're attached to the `tool0` link.

### Convert catalogue models

`convert` exports a model from NOVA's catalogue. NOVA only reports a model's joint
limits for a running controller, so `convert` creates a temporary virtual controller
and deletes it afterwards. If the instance already has controllers, it asks for
confirmation first. Pass `-y` / `--yes` to skip the prompt.

```bash
# a short name or the full model ID
nova2urdf convert crx25ia

# every model, four controllers at a time
nova2urdf convert --all --yes
```

### Validate exports

```bash
nova2urdf validate robot_exports/fanuc_crx25ia/fanuc_crx25ia.urdf
nova2urdf validate robot_exports/
```

### Global options

These go before the command, for example `nova2urdf -o ./robots convert ur5e`.

| Option | Effect |
|--------|--------|
| `--nova-url URL` | The NOVA instance. Overrides `NOVA_API_URL` |
| `-o`, `--output-dir DIR` | Where exports are written. Default: `robot_exports/` |
| `--mesh-format {glb,obj}` | Visual mesh format. Use `obj` for tools that can't read GLB: RViz, MuJoCo, PyBullet, Gazebo |
| `--materials {glb,usd}` | Where visual materials come from. `usd` needs the `usd` extra |
| `-q`, `--quiet` | Errors only |
| `-v`, `--verbose` | Debug logging |
| `--version` | Print the version |

### Shell completion

```bash
eval "$(nova2urdf completions bash)"    # Bash
eval "$(nova2urdf completions zsh)"     # Zsh
nova2urdf completions fish | source     # Fish
```

## Python usage

`export_controller` returns a `UrdfPackage`: the URDF and every mesh it references,
held in memory as bytes. Nothing is written to disk unless you call `write`.

```python
import asyncio

import httpx

from nova2urdf import export_controller


async def main() -> None:
    async with httpx.AsyncClient(timeout=30) as client:
        package = await export_controller(
            client,
            "my-robot",
            tcp_name="Gripper",          # optional: where tool0 sits
            mesh_format="glb",           # "glb" (default) or "obj"
            nova_url="http://<your-nova-host>",
        )
        if package is None:
            raise RuntimeError("export failed; see the log for why")

        package.urdf                              # the URDF, as XML text
        package.files["visual/mg0_link_1.glb"]    # each mesh, as bytes
        package.write("robot_exports/my-robot")   # returns the URDF's path


asyncio.run(main())
```

Export again whenever the TCPs, tool colliders or joint limits change: each call reads
them from the controller. Several exports can run concurrently.

**Authentication and several instances.** Credentials go on the `httpx` client.
`nova_url=` and `cell=` point one call at one instance; `nova_target()` does the same
for a whole block, so one process can talk to several instances:

```python
from nova2urdf import nova_target

async with httpx.AsyncClient(
    headers={"Authorization": f"Bearer {token}"}, timeout=30
) as client:
    with nova_target("http://<your-nova-host>", cell="cell"):
        package = await export_controller(client, "my-robot")
```

**Errors.** When a model's meshes yield no usable geometry, the export raises
`MeshExtractionError`. It doesn't return a URDF with correct kinematics and no meshes,
which would look like success and show an invisible robot:

```python
from nova2urdf import MeshExtractionError

try:
    package = await export_controller(client, "my-robot")
except MeshExtractionError:
    ...  # the model's meshes are unusable
```

**Catalogue models.** `export_robot(client, model_id)` exports a catalogue model rather
than a configured controller. Like `convert`, it starts a temporary virtual controller
to read the joint limits, which takes far longer than the export itself.

## Configuration

| Environment variable | Default | Meaning |
|----------------------|---------|---------|
| `NOVA_API_URL` | none | NOVA instance base URL. Required unless you pass `--nova-url` |
| `NOVA_CELL` | `cell` | Cell the controllers live in |
| `NOVA_ACCESS_TOKEN` | none | Bearer token, for instances that require one |

The command-line tool also reads a `.env` file from the working directory.

## What an export contains

```text
robot_exports/fanuc_crx25ia/
├── fanuc_crx25ia.urdf
├── motion_groups.json      # controller exports: joints and frames per motion group
├── visual/
│   ├── link_0.glb
│   └── ...
└── collision/
    ├── link_1_link_1_compound_convexHull_0.stl
    └── ...
```

| In the URDF | Comes from |
|-------------|------------|
| Joint origins and axes | The model's DH parameters |
| Joint position and velocity limits | The controller's motion group |
| Joint effort limits | The model, where NOVA has them (Universal Robots, for example) |
| Visual meshes | The model's meshes, as GLB, or as OBJ+MTL with `--mesh-format obj`. Both keep material colours |
| Collision meshes | The model's convex hulls, as STL |
| `tool0` frame | The flange, or the TCP you pass |
| Tool collision shapes | The cell's collision store, as native URDF spheres, boxes and cylinders, or as STL for convex hulls |

Capsule-shaped tool colliders become cylinders, since URDF has no capsule.

### Robots with several motion groups

Every motion group of a controller becomes one chain of a single URDF. When the parts
sit on each other, for example arms mounted on a carrier, the URDF is one tree, so
moving the carrier's joints moves everything mounted on it.

Names get a per-motion-group prefix (`mg1_q1`, `mg1_link3_passive`, `mg1_tool0`), and
`motion_groups.json` lists each motion group's joints in order, plus its frames:

```json
{
  "controller": "my-robot", "urdf": "my-robot.urdf", "length_unit": "m",
  "motion_groups": [
    {"motion_group": "1@my-robot", "model": "ARM_A", "prefix": "mg1_",
     "joints": ["mg1_q1", "..."], "flange_link": "mg1_link7_passive",
     "tool0_link": "mg1_tool0", "tcp": "Flange",
     "tcps": {"Gripper": "mg1_tcp_Gripper"},
     "parent_link": "mg0_link4_passive"}
  ]
}
```

A controller with a single motion group exports without prefixes.

## Limitations

The URDFs are **kinematic, visual and collision models**. They suit visualization,
motion planning and collision checking. **Physics simulation** needs more work, because
the exports carry no dynamics:

- **No `<inertial>` elements:** mass, centre of mass and inertia tensor are missing.
- **No `<dynamics>` elements:** damping and friction are missing.

NOVA doesn't serve per-link dynamics through its API. To simulate anyway:

1. **From datasheets:** add inertial data from the manufacturer's datasheet. Universal
   Robots, for example, publishes link masses and inertias.
2. **From the collision meshes:** estimate the inertia, for example with trimesh's
   volume estimation or MuJoCo's `mj_setTotalmass`.
3. **In MuJoCo:** let `compiler/inertiafromgeom` compute the inertia from collision
   geometry with an assumed density.

## License

Apache License 2.0. See `LICENSE`.

## Acknowledgements

The DH-to-URDF conversion is based on [DH2URDF](https://github.com/AdoHaha/DH2URDF).
