# Robots_CopyGripperAsUmiRig

`POST /v1/accounts/{account}/robots/{robot}:copyAsUmiRig`

Copies a gripper template into a UMI rig and returns the rig: a workspace-level
gripper (kind GRIPPER, `gripper_tcp.rig_kind` "umi-handheld") that is a
snapshot copy of the template's URDF, meshes, jaw and TCP as they are now,
with `source_template` naming the template. It has no `rig_key`: a
recording names it by its name or id (ComposeUploadRig `gripper`). It is a
recording rig only once it has a tracker-to-TCP
calibration: import tcp_calibration.json with ImportGripperCalibration. A
workspace may have any number of UMI rigs; they are made from templates
only, so no upload can be recorded on a rig no one made.

Needs rigs:write on the organization as well as robots:write (a
workspace-level gripper write). NOT_FOUND for a template of another
workspace. FAILED_PRECONDITION when `name` is no gripper template (a robot,
a robot template, a UMI rig or a robot's own gripper), when it
has no URDF, or is being imported. INVALID_ARGUMENT for a name over 64
characters or one a gripper may not have (a rig key such as
"umi-handheld"). A name the workspace has gets the next free " (2)", " (3)"
... suffix. ABORTED when the template changed while it was copied: try again.
A retry with the same `request_id` returns the first call's rig. The rig's
registration is an event naming the template.

- Base URL: `https://junction-api.qualiastudios.dev`
- Authentication: an API key in the `X-API-Key` header

## Parameters

- `account` (path, string, required): The account id.
- `robot` (path, string, required): The robot id.

## Request body

Required: `application/json` `qualia.v1.CopyGripperAsUmiRigRequest`

## Responses

- `200` OK: `application/json` `qualia.v1.Robot`
- `default` Default error response: `application/json` `google.rpc.Status`

## Schemas

```yaml
qualia.v1.CopyGripperAsUmiRigRequest:
  required:
    - name
  type: object
  properties:
    name:
      type: string
      description: |-
        accounts/{account}/robots/{robot} of kind GRIPPER in the workspace, with no
        `rig_kind` and no `owner`: the gripper template to copy.
    displayName:
      type: string
      description: |-
        The rig's name, 1 to 64 characters, unique among the workspace's robots and
        workspace-level grippers. Empty: "<template name> UMI".
    requestId:
      type: string
      description: |-
        Idempotency key (AIP-155): a retry with the same id within 24 hours returns
        the first call's rig instead of making a second one.
    sourceTcpEtag:
      type: string
      description: |-
        Robot.GripperTcp.etag of the template as read (empty when it has no TCP), so
        a template that changed since is ABORTED (try again) rather than copied
        half-way, as SetRobotGripper checks `source_tcp_etag`.
    sourceModelEtag:
      type: string
      description: RobotModel.etag of the template as read; see source_tcp_etag.
  description: Request for Robots.CopyGripperAsUmiRig.
qualia.v1.Robot:
  required:
    - displayName
  type: object
  properties:
    name:
      type: string
      description: accounts/{account}/robots/{robot}
    displayName:
      type: string
      description: Unique per account.
    robotType:
      type: string
      description: |-
        The robot type key (fleet.robot_type.key), e.g. "openarm", "franka_fr3".
        Empty for a robot defined only by an imported URDF.
    serialNumber:
      type: string
      description: Serial number, free text.
    hasModel:
      readOnly: true
      type: boolean
      description: Whether the robot has a model (URDF) configured.
    configDigest:
      readOnly: true
      type: string
      description: |-
        RobotModel.config_digest, empty without a model: what reach results are
        keyed by. Always empty for a gripper design.
    createTime:
      readOnly: true
      type: string
      description: When it was registered.
      format: date-time
    updateTime:
      readOnly: true
      type: string
      description: When it last changed.
      format: date-time
    etag:
      type: string
      description: Sent back on UpdateRobot.
    kind:
      enum:
        - KIND_UNSPECIFIED
        - ROBOT
        - GRIPPER
        - ROBOT_TEMPLATE
      type: string
      description: |-
        Robot, robot template or gripper design. Set by CreateRobot (unset is ROBOT)
        and never changed.
      format: enum
    gripperTcp:
      readOnly: true
      allOf:
        - $ref: "#/components/schemas/qualia.v1.Robot_GripperTcp"
      description: "Kind GRIPPER only: its TCPs."
    carriedGripper:
      readOnly: true
      allOf:
        - $ref: "#/components/schemas/qualia.v1.Robot_CarriedGripper"
      description: "Kind ROBOT only: the gripper it carries, if any."
    owner:
      type: string
      description: |-
        Kind GRIPPER only: accounts/{account}/robots/{robot} of the robot that
        owns this gripper. Empty for a workspace-level gripper (the handheld's, or
        a library gripper). Set on CreateRobot (with kind GRIPPER) to make the
        named robot's own gripper, which that robot carries from then on (with no
        mounts); never changed after.
    stagedGripperSupported:
      readOnly: true
      type: boolean
      description: |-
        The server supports exact staged gripper copy and fenced replacement.
        Absent on older servers; clients must not assume those guarantees.
    thumbnailSha256:
      readOnly: true
      type: string
      description: |-
        The sha256 (lowercase hex) of the robot's thumbnail, a picture of its
        model at the default pose that fleet draws when the model is imported
        (the bytes are GetRobotThumbnail's). Empty when it has none yet, or when
        the model was replaced since it was drawn, so a replaced model never shows
        its old picture.
    solver:
      readOnly: true
      enum:
        - SOLVER_UNSPECIFIED
        - PYROKI
        - DLS
        - MINK
      type: string
      description: |-
        RobotModel.solver, so a reader of the robot knows whether it has one
        without reading the model (whose signed URLs cost fleet more than the
        answer is worth). SOLVER_UNSPECIFIED: the robot has no solver, or no model;
        `has_model` tells which. Always SOLVER_UNSPECIFIED for a gripper design.
        fleet always sets it, SOLVER_UNSPECIFIED included, so a reader that finds
        it unset is talking to an older fleet, which does not know the field, and
        reads the model's solver instead.
      format: enum
    sourceTemplate:
      readOnly: true
      type: string
      description: |-
        accounts/{account}/robots/{robot} of the template this record was copied
        from: for a robot, the robot template CopyRobot copied (or the template of
        the robot it copied); for a gripper, the gripper template it was copied from
        (a UMI rig made by CopyGripperAsUmiRig, or the copy a robot owns). Empty when
        it was made another way, copied from something that was no template, or its
        template was deleted. It is provenance only: nothing is re-synced from it.
  description: |-
    A robot the account keeps (fleet.robots). Episodes can be recorded on it
    (Dataset.source_robot) and played on it (Dataset.target_robot).

    A gripper design (`kind` GRIPPER) is the same record: a URDF of one gripper
    with the TCP of each hand on it (`gripper_tcp`), which is also what
    a handheld rig's trackers are calibrated to and what Rig.gripper points at. Everything that takes a robot name works on it (upload, model,
    delete), but it is no robot: ListRobots leaves it out unless asked for
    grippers, so it is never the default target, and Robot.config_digest is
    always empty (its RobotModel keeps one), so no reach result is keyed by it.
    It lives in one workspace. A workspace-level gripper is one of two things: a
    gripper template (`gripper_tcp.rig_kind` empty), the design a robot's gripper
    or a UMI rig is copied from, or a UMI rig (`gripper_tcp.rig_kind`
    "umi-handheld"), the capture rig a handheld recording is converted
    with: made by CopyGripperAsUmiRig from a gripper template and calibrated with
    ImportGripperCalibration. A workspace has as many UMI rigs as someone made
    and none otherwise: nothing makes one on listing, and a UMI rig is deleted
    like any other robot.

    A robot template (`kind` ROBOT_TEMPLATE) is the model of a robot
    that rigs are made from: a URDF with its meshes and arms, and nothing a rig
    adds (no solver, gripper, serial number, credentials, datasets or reach
    results). CopyRobot makes a rig from it. The copy is a snapshot and shares
    nothing with the template afterwards, and deleting the template never
    touches a rig.

    A robot carries a gripper on its flanges (`carried_gripper`), set with
    SetRobotGripper, and owns it: attaching a gripper copies its URDF, TCP and jaw
    into a gripper design of the robot's own (`owner` names the robot), so editing
    it changes that robot only. An owned gripper is carried by its owner alone, is
    included when listing grippers as a source to copy, is deleted with its robot,
    by detaching it, or by DeleteRobot on the gripper, and
    needs robots:write to change (a workspace-level gripper, a gripper template or
    a UMI rig, needs rigs:write). Everything that takes a gripper name works on
    it (GetRobot, the model, SetGripperTcp, ImportGripperCalibration).

    Served by fleet.
qualia.v1.Robot_GripperTcp:
  type: object
  properties:
    hands:
      readOnly: true
      type: array
      items:
        $ref: "#/components/schemas/qualia.v1.GripperTcp_Hand"
      description: "The hands that have a TCP: none, one or both sides."
    calibration:
      readOnly: true
      allOf:
        - $ref: "#/components/schemas/qualia.v1.TcpCalibration"
      description: |-
        The hands' `tracker_to_tcp` as a calibration, the form
        Datasets.ResolvePoses takes. Set only when `resolves_uploads`.
    rigKey:
      readOnly: true
      type: string
      description: |-
        The key a recording may name this gripper by, e.g. "umi-handheld" on a
        workspace's first UMI rig, which also stands for the ingest rig kind of the
        same name (`rig_kind`). Unique among workspace-level grippers. Empty for a
        gripper that has none: a gripper template, a UMI rig made by CopyGripperAsUmiRig
        (named by its name or id), and the gripper a robot owns (see
        `source_rig_key`); a TCP on it serves robots only. Nothing makes a keyed
        gripper any more: the key stays on the ones that have it.
    filename:
      readOnly: true
      type: string
      description: The calibration file's name, or "tcp-matrix:<gripper name>" for a matrix.
    deriveDate:
      readOnly: true
      type: string
      description: |-
        "YYYY-MM-DD": the file's, or the day the matrix was set (UTC). Empty when
        a file gave none.
    stale:
      readOnly: true
      type: boolean
      description: |-
        Whether the gripper's URDF changed since a TCP in a link was set: a
        re-import changed its digest or took the link away. A stale TCP is shown
        with a warning and is never composed into a robot's arm; set it again,
        every hand that has one in the same call.
    tcpSet:
      readOnly: true
      type: boolean
      description: "Robots can use it: every hand has its TCP in a link and it is not stale."
    resolvesUploads:
      readOnly: true
      type: boolean
      description: |-
        Uploads can be resolved with it: both hands have a `tracker_to_tcp`, and
        the calibration has a file name and a date.
    etag:
      readOnly: true
      type: string
      description: Sent back on SetGripperTcp and ImportGripperCalibration.
    sourceRigKey:
      readOnly: true
      type: string
      description: |-
        The `rig_kind` (else the `rig_key`) of the gripper this one was copied from,
        e.g. "umi-handheld", for the gripper a robot owns; empty otherwise. It says
        the gripper is a copy of a UMI rig's gripper (and so has its controller to
        TCP transform), which a copy cannot claim as its own `rig_kind`: uploads are
        resolved with a UMI rig.
    rigKind:
      readOnly: true
      type: string
      description: |-
        The ingest rig kind this gripper converts a recording with, e.g.
        "umi-handheld": what Dataset.poses_rig names. Set exactly on a UMI rig (a
        workspace-level gripper made by CopyGripperAsUmiRig, or the one a workspace had
        before: its `rig_key` is the same string). Several UMI rigs of one kind may exist in
        a workspace. Empty on a gripper template and on a gripper a robot owns.
  description: |-
    A gripper's tool centre points (TCP), one per hand. Only a gripper has one.
    It is set with SetGripperTcp (a 4x4 matrix) or ImportGripperCalibration
    (a tcp_calibration.json), never by UpdateRobot, and lives in the
    gripper's workspace like the gripper.
qualia.v1.GripperTcp_Hand:
  type: object
  properties:
    side:
      readOnly: true
      enum:
        - SIDE_UNSPECIFIED
        - SIDE_LEFT
        - SIDE_RIGHT
      type: string
      description: Which hand.
      format: enum
    link:
      readOnly: true
      type: string
      description: |-
        The link of the gripper's URDF that `tcp` is expressed in, with the
        side resolved ("openarm_left_ee_base_link"). Empty when the hand has
        only a tracker_to_tcp.
    tcp:
      readOnly: true
      allOf:
        - $ref: "#/components/schemas/qualia.v1.Matrix4"
      description: The TCP's pose in `link`'s frame. Empty when the hand has none.
    trackerToTcp:
      readOnly: true
      allOf:
        - $ref: "#/components/schemas/qualia.v1.Transform"
      description: |-
        The TCP's pose in the tracker's frame: what uploads are resolved with.
        Unset when the hand has none, so it serves robots only.
  description: One hand's TCP.
qualia.v1.Matrix4:
  type: object
  properties:
    m:
      type: array
      items:
        type: number
        format: double
      description: m[0..3] is the first row, m[12..15] the last.
  description: |-
    A 3D rigid transform as a 4x4 matrix: 16 numbers, row-major, translation in
    metres in the last column, the bottom row 0 0 0 1. Where it is used says
    whose pose it is and in which frame (Robot.GripperTcp.Hand.tcp: the TCP in
    a link's frame). Unlike Transform it has no pitch pole to lose a rotation at.
qualia.v1.Transform:
  type: object
  properties:
    xyz:
      type: array
      items:
        type: number
        format: double
      description: x, y, z in metres.
    rpy:
      type: array
      items:
        type: number
        format: double
      description: roll, pitch, yaw in radians.
  description: |-
    A 3D rigid transform: translation in metres and roll, pitch, yaw in radians
    (URDF convention, extrinsic XYZ).
qualia.v1.TcpCalibration:
  required:
    - filename
    - hands
  type: object
  properties:
    filename:
      type: string
      description: The uploaded file's name, e.g. "tcp_calibration.json".
    deriveDate:
      type: string
      description: |-
        When the calibration was made, "YYYY-MM-DD": the leading date of the
        file's `calibrated` or else its `derived` text. Empty when the file gives
        none.
    hands:
      type: array
      items:
        $ref: "#/components/schemas/qualia.v1.TcpCalibration_Hand"
      description: Both hands, left then right.
    eeLink:
      type: string
      description: |-
        The link of the rig's gripper design that `Hand.ee_to_tcp` is a pose in,
        with `{side}` for "left" or "right" (the gripper joint's convention in
        RobotModel.Gripper), e.g. "openarm_{side}_ee_base_link". Read from the
        file's own description of its end-effector frame. Empty when the file
        names none, lacks the end-effector poses, or they disagree with its
        controller_to_tcp (then no `ee_to_tcp` is set either). Display only.

        `ee_to_tcp` is drawn in this link's own frame, which must be the plain
        end-effector frame the file means. An export whose link sits elsewhere
        draws the TCP off by the difference: an OpenArm exported with the xacro
        default collapse_internal_empty_links puts openarm_<side>_ee_base_link at
        link7, 20.5 mm behind the frame the calibration is in.
  description: |-
    A handheld rig's calibration of its trackers to the gripper's tool centre
    point (TCP, the midpoint of the closed fingertips). It is what a tcp_calibration.json holds as `<side>.controller_to_tcp`: the TCP's pose in
    the tracker's frame, so a pose recorded at a tracker becomes the pose of the
    TCP by composing it with this transform.

    A file that also names the gripper's end-effector link gives the same TCP in
    that link's frame (`ee_link`, `Hand.ee_to_tcp`), which is what a rig's
    gripper design (Rig.gripper) draws the TCP with. Those two are for display:
    poses are resolved and uploads converted with `tracker_to_tcp`, `filename`
    and `derive_date` alone, never with `ee_to_tcp`. So compare two calibrations
    on those, `hands[].side` and `hands[].tracker_to_tcp` only: a copy kept
    elsewhere (a dataset keeps the calibration its poses were resolved with)
    carries just those, and a whole-message comparison would call every
    calibration that has the end-effector fields replaced.
qualia.v1.TcpCalibration_Hand:
  required:
    - side
    - trackerToTcp
  type: object
  properties:
    side:
      enum:
        - SIDE_UNSPECIFIED
        - SIDE_LEFT
        - SIDE_RIGHT
      type: string
      description: Which hand.
      format: enum
    trackerToTcp:
      allOf:
        - $ref: "#/components/schemas/qualia.v1.Transform"
      description: |-
        The TCP's pose in the tracker's frame: xyz in metres and rpy in radians
        (URDF convention, extrinsic XYZ), the file's rotation matrix converted.
        A rotation whose pitch is within a hair of +-90 degrees has no unique
        roll and yaw; it is stored with roll 0.
    eeToTcp:
      allOf:
        - $ref: "#/components/schemas/qualia.v1.Transform"
      description: |-
        The same TCP in the frame of the gripper design's `ee_link` for this
        hand, in the form of `tracker_to_tcp` (so the same pitch limit). Display
        only. Set with `ee_link` or not at all, and only when it composes with
        the file's controller_to_ee to the file's controller_to_tcp, which is
        what makes the TCP drawn the TCP used.
  description: One hand's transform.
qualia.v1.Robot_CarriedGripper:
  type: object
  properties:
    gripper:
      readOnly: true
      type: string
      description: |-
        accounts/{account}/robots/{robot} of kind GRIPPER, in the robot's
        workspace: the robot's own gripper (its `owner` is the robot).
    mounts:
      readOnly: true
      type: array
      items:
        $ref: "#/components/schemas/qualia.v1.CarriedGripper_Mount"
      description: One per arm side the gripper is mounted on.
    etag:
      readOnly: true
      type: string
      description: Sent back on SetRobotGripper.
    displayName:
      readOnly: true
      type: string
      description: |-
        The gripper's display name (the one it was copied from), so a robot can
        say "gripper: KIWI gripper pair" without reading the gripper.
  description: |-
    The gripper a robot carries on its flanges, and where each one sits on its
    arm. Only a robot (kind ROBOT) has one.
qualia.v1.CarriedGripper_Mount:
  required:
    - side
  type: object
  properties:
    side:
      enum:
        - SIDE_UNSPECIFIED
        - SIDE_LEFT
        - SIDE_RIGHT
      type: string
      description: Which arm.
      format: enum
    parentLink:
      type: string
      description: |-
        The robot's link the gripper hangs off: it must be joined to the arm's
        `hand_link` by fixed joints only.
    matrix:
      allOf:
        - $ref: "#/components/schemas/qualia.v1.Matrix4"
      description: |-
        The pose of the gripper's hand link (its TCP `link`) in `parent_link`'s
        frame. Ignored when `baked`. The mount is for the link the hand's TCP is
        in when it is set, which must exist then; a TCP set in another link
        later composes nothing until the mount is set again.
    baked:
      type: boolean
      description: |-
        The robot's own URDF already has the gripper built in, as the gripper
        design has it: the mount is the identity on the robot's link of the
        gripper's link name. The server checks that the two URDFs agree on where
        that link is (FAILED_PRECONDITION when they do not) and sets
        `parent_link` and `matrix` itself.
    guessed:
      type: boolean
      description: The mount was filled in by a guess and has not been confirmed.
  description: Where a gripper's hand link sits on one arm.
google.rpc.Status:
  oneOf:
    - $ref: "#/components/schemas/ApiError"
    - $ref: "#/components/schemas/GatewayError"
  description: "The body of a failed call: one of the two shapes below."
ApiError:
  title: API error
  type: object
  required:
    - code
    - message
    - retryable
    - details
  properties:
    code:
      type: string
      description: The gRPC code name in upper case, e.g. `NOT_FOUND`.
    message:
      type: string
    retryable:
      type: boolean
      description: Whether the same call may succeed if retried.
    details:
      type: array
      items:
        type: object
      description: "Empty, except when a dataset copy or move is refused: then one `google.rpc.ErrorInfo` with the refusal's `reason` code, its `domain` and, in `metadata`, the datasets it names."
  description: A refusal from the API itself.
GatewayError:
  title: Gateway error
  type: object
  required:
    - detail
  properties:
    detail:
      type: string
      description: What went wrong.
  description: A refusal from the gateway before the call reaches the API, for example `401` bad or missing API key, `403` key lacks the scope or environment closed, `413` body too large.
```
