# Add a robot model

> Create a new RosieOS robot description, from URDF and meshes through config.json, rtcore_definition.json and collision spheres, register it with the manifest tool, and pin it from a machine config.

URL: https://advancedmetalresearch.com/docs/guides/add-a-robot-model
Section: RosieOS docs / Guides
Last updated: 2026-10-10

This guide creates a new robot description in `robot_description/robots/<model_id>/`, checks it, and pins it from an rt-core machine config so a cell can run it and OLP can plan for it. Read [Robot description and coordinate frames](https://advancedmetalresearch.com/docs/concepts/robot-description-and-frames) first; every field is in [Robot description files](https://advancedmetalresearch.com/docs/reference/robot-description).

The checks along the way:

```bash
python3 robot_description/tools/manifest.py --check          # manifest and meshes
(cd robot_description/go && go run ./cmd/identity ../robots/<model_id>)
(cd rt-core && build/rtctl compile --config config/machines/<your-machine>.json --out build/config)
```

## Before you start

- Pick a model id: ASCII letters, digits and underscores, for example `rosie_1600_v1`. It is the directory name, the URDF robot name, the `model_id` in every file, and the cell id that programs carry. Changing it later means a new model.
- Have the robot's kinematics, joint limits and meshes, and its drive gearing, encoder scale and joint directions from your mechanical design.
- Build `rt-core` (`make -C rt-core control`) and install the weld planner's `motion` environment if you will fit spheres (see [Run the weld planner](https://advancedmetalresearch.com/docs/guides/run-the-weld-planner)).

Everything you register becomes part of the model's identity, and changing it later invalidates every plan made against it. That is intended: finish the files before you pin them.

## 1. Create the directory

```bash
mkdir -p robot_description/robots/rosie_1600_v1/meshes
```

Put the link meshes in `meshes/`. Keep CAD exports, the scripts that produced the files and measurement reports in a `provenance/` folder. They are not registered, so editing them never changes the identity.

## 2. Write `robot.urdf` and `robot.srdf`

- Set `<robot name="rosie_1600_v1">`, equal to the directory name.
- Give every movable joint a bounded `<limit lower upper velocity>`, in rad and rad/s. rt-core takes each robot-bound axis's travel and maximum velocity from here, and refuses a joint without bounded limits.
- Reference meshes as `package://<anything>/meshes/<file>`.
- End the arm in a `tool0` link at the tool centre point, as the Rosie models do, and put the torch's electrode along +x of that frame.
- In `robot.srdf`, define a `manipulator` group with the planned joints and a `home` group state.

Name the arm links `link_1` to `link_6` if you can. The sphere-fitting tool assumes those names.

The sphere tool in `urdf/v1/sphere_tool` can also help find joint limits by driving each joint to where the arm stops. See [step 5](https://advancedmetalresearch.com/docs/guides/add-a-robot-model#spheres).

## 3. Write `config.json`

Copy a sibling's `config.json` and replace every value with this robot's. State only what URDF cannot: do not repeat a URDF number.

robot_description/robots/rosie_1600_v1/config.json (shape):

```json
{
  "schema": "rosie.robot-config.v1",
  "model_id": "rosie_1600_v1",
  "frames": { "base": "world", "tool": "tool0", "work": "world" },
  "kinematic_correction_caps": { "xyz_m": 0.002, "rpy_rad": 0.017453292519943295 },
  "planning": {
    "speed_ceiling_rad_s": 1.5,
    "joints": { "J1": { "velocity_rad_s": 1.0, "acceleration_rad_s2": 2.0 } }
  },
  "axes": { "driven": ["J1", "J2", "J3", "J4", "J5", "J6"], "held": {} },
  "reset_pose": { "J1": 0.0 },
  "torch": { "tool_frame": "tool0", "electrode_axis": [1.0, 0.0, 0.0] }
}
```

The loader refuses:

- a `schema` or `model_id` that does not match
- a planning `velocity_rad_s` above that joint's URDF `velocity`, or a `speed_ceiling_rad_s` above the fastest URDF joint
- a non-positive rate, a negative cap, or a non-finite held value
- any joint in `planning`, `axes` or `reset_pose` that is not a revolute URDF joint
- unknown fields

Give **every** joint an `acceleration_rad_s2`. rt-core uses it for trajectories and jog on each robot-bound axis and refuses to compile without it (`robot_acceleration_required`).

For a positioner, add `frames.work_surface` so the work frame sits on the table surface rather than at the link origin, and hold any axis the planner should not move. See [Cells, machines and positioners](https://advancedmetalresearch.com/docs/concepts/cells-and-positioners#the-h-frame-positioner) for the Rosie 1400 example.

## 4. Write `rtcore_definition.json`

This file tells rt-core how each joint maps to a drive. Start from the annotated template `rt-core/config/templates/robot.json`: every field there has a `_doc` sibling with its unit, range and default. Remove the `_doc` fields and fill in your robot.

> [!NOTE] The gear ratio, encoder counts, joint signs and encoder battery facts come from your robot's mechanical design and drives. These docs describe the fields but give no values. Every numeric fact needs a `_source` ("cited: …") or `_unverified` ("unverified: …") sibling, or compile refuses it.

Checklist:

- `schema` is `rosie.robot-definition.v1`, and `robot_id` equals the directory name.
- `calibration` is `{"identity": "home_calibration", "note": "…"}`. Home offsets are never stored here.
- `defaults` names the drive config (`drive_profile`), `coordinate_evidence_mode` and `home_policy` (`required` unless you have a reason).
- `joints[]` has one entry per movable URDF joint, at most 16: `name`, `kind` (`arm`, `positioner` or `external`), `urdf_joint`, `cartesian_participates`, `gear_ratio`, `motor_encoder_counts_per_rev`, `sign`, and, for joints on a supported absolute-encoder drive, `encoder_battery` with its source.
- `arm` joints must map to a URDF joint. Only an `external` joint with no URDF joint may declare `mechanical_travel`.

A robot-bound machine cannot override any of these, so get them right here.

## 5. Fit the collision spheres

The weld planner refuses a model without `spheres.json`. Author a first model in the sphere tool, then refine it:

```bash
(cd urdf/v1/sphere_tool && npm ci && npm run build)
python3 urdf/v1/sphere_tool/serve.py 8795 rosie_1600_v1     # open http://127.0.0.1:8795/
# save the result as robot_description/robots/rosie_1600_v1/spheres.json

cd weld_planner/v1
pixi run -e motion python tools/fit_spheres.py --cell rosie_1600_v1
pixi run -e motion motion-spheres
```

`fit_spheres.py` refits an existing `spheres.json` in place so every sphere sits inside its link, and stamps the URDF and mesh hashes it fitted against into `provenance`. By default it refits `link_3` to `link_6`; pass `--links` to choose others. After a URDF edit that moves no mesh, `--restamp` updates the hashes and changes nothing else. `motion-spheres` reports which links are covered.

## 6. Register the files

```bash
python3 robot_description/tools/manifest.py rosie_1600_v1
# manifest: rosie_1600_v1 <n> files sha256:<64 hex>
python3 robot_description/tools/manifest.py --check
```

The manifest registers `robot.urdf`, `robot.srdf`, `config.json`, `spheres.json`, `rtcore_definition.json` and everything in `meshes/`. Do not add a `machine_planning_calibration.json` to the description: that file belongs to a cell. `--check` also fails if the URDF names a mesh that is not in `meshes/`.

Print the identity with the Go tool too; both must agree:

```bash
(cd robot_description/go && go run ./cmd/identity ../robots/rosie_1600_v1)
```

## 7. Pin it from a machine config

Create a machine config under `rt-core/config/machines/` that pins the directory and its identity, and maps one axis per definition joint:

rt-core/config/machines/bench/rosie1600.json (shape):

```json
{
  "schema_version": 1,
  "backend": "live",
  "cycle_ns": 1000000,
  "robot": {
    "robot_description": {
      "path": "robot_description/robots/rosie_1600_v1",
      "sha256": "sha256:<identity from step 6>",
      "source": "cited: robot_description/robots/rosie_1600_v1"
    }
  },
  "axes": [
    { "robot_joint": "J1", "slave_position": 0,
      "limits": { "max_target_lead": 0.02, "following_error": 0.04, "following_error_timeout_ms": 100,
                  "completion_tolerance": 0.0002, "completion_timeout_ms": 500 } }
  ],
  "max_cycle_lateness_ns": 20000000,
  "runtime": { "cpu": 2, "priority": 90 }
}
```

Robot-bound axes carry only `robot_joint`, `slave_position` and the tracking limits. They inherit travel, velocity, acceleration, gearing and Home policy from the description, and compile refuses any attempt to redeclare them. List a definition joint that has no drive on this machine in `robot.absent_joints`.

Compile it:

```bash
cd rt-core
build/rtctl compile --config config/machines/bench/rosie1600.json --out build/config
```

| If compile says | Fix |
|---|---|
| `robot_description_mismatch: … hashes to X and the machine pins Y` | Update `sha256` to the current identity. |
| `robot_definition_unavailable` | Register `rtcore_definition.json` in the manifest. |
| `robot_acceleration_required` | Add `acceleration_rad_s2` for that joint in `config.json`. |
| `robot_joint_missing` | Add an axis for the joint, or list it in `robot.absent_joints`. |
| `robot_urdf_limits_required` | Remove the limit, velocity or gearing field from the axis. |

Every reason is listed in [robot compile refusals](https://advancedmetalresearch.com/docs/reference/configuration#robot-compile-refusals). Then deploy it as in [Install rt-core on a cell host](https://advancedmetalresearch.com/docs/guides/install-on-a-cell-host), and update every cell pin with the new configuration digest.

## 8. Plan against it

OLP lists every description with a URDF, so the new model appears in its robot catalogue. The planner finds it by directory name. A program planned for it records the model id and identity, and Load refuses it on a cell with any other description (`robot_cell_mismatch`).

To try the model in simulation, write a `"backend": "simulation"` machine that pins it the same way. Home works on a simulated robot-bound machine only if the definition's drive profile supports Home on the simulated bus. The shipped Rosie 1400 simulation machine does not, which is why the dev stack's default simulated cell uses flat axes. See [Run everything in simulation](https://advancedmetalresearch.com/docs/guides/run-in-simulation).

## When the robot changes

Edit the files, rerun `manifest.py <model_id>`, and update the identity in every machine config that pins the model. Recompile, redeploy and update the cell pins. Plans made against the old identity are refused and must be planned again. That is the point: the plan was for a different robot.

## Sources

Written from these files in the RosieOS repository (https://github.com/advanced-metal-research/RosieOS):

- `robot_description/tools/manifest.py:1-131`
- `robot_description/go/identity.go:60-118`
- `robot_description/go/description.go:122-285`
- `robot_description/go/cmd/identity/main.go:1-32`
- `robot_description/robots/rosie_1420_v1/config.json`
- `robot_description/robots/rosie_1400_v3/robot.srdf`
- `rt-core/config/templates/robot.json:1-109`
- `rt-core/config/templates/machine.json (robot, axes robot-bound branch)`
- `rt-core/tools/rtctl/robot_definition.go:130-160,250-290,313-420,426-500,500-735`
- `rt-core/config/machines/simulation/simulation-rosie1400.json`
- `weld_planner/v1/tools/fit_spheres.py:1-60,168-226`
- `weld_planner/v1/pixi.toml:144`
- `urdf/v1/sphere_tool/serve.py:1-45`
- `urdf/v1/sphere_tool/package.json:6-11`
- `robot_description/go/description.go:327-360`
- `motion-server/v1/local-rt-core.sh:17-60`
