> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zeroruntime.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# RobotDescriptor

> Describe your machine's joints and what their numbers mean, so both ends agree.

A descriptor is your machine's schema: what the joints are called, in what
order, and what their numbers mean. Both machines return one from
`descriptor()`, and both must describe the same machine.

## Define one

Most machines are one set of joints in one unit, which is what
`single_group_descriptor()` builds.

```python theme={null}
from zeroruntime.teleops import single_group_descriptor, Units

JOINTS = ("shoulder", "elbow", "wrist", "gripper")

class MyArm(Follower):
    def descriptor(self):
        return single_group_descriptor("my-arm", "my-arm", JOINTS,
                                       units=Units.DEG, control_hz=50)
```

<Note>
  `single_group_descriptor()` is only a shortcut. It builds a `RobotDescriptor`
  with one `Group` called `main`, and you can build that yourself whenever you
  prefer, as the [mixed units](#machines-that-mix-units) example does.
</Note>

<ParamField path="robot_id" type="str" required>
  A name for this machine. The two ends use different ids, such as `my-arm` and
  `my-controller`.
</ParamField>

<ParamField path="profile" type="str" required>
  What kind of machine it is. **Both ends must pass the same profile**, since
  this is what says the controller is driving this type of arm.
</ParamField>

<ParamField path="joints" type="tuple[str, ...]" required>
  The joint names, **in order**. Order is part of the schema, so both ends must
  list them identically.
</ParamField>

<ParamField path="units" type="Units" default="DEG">
  What the numbers mean: `DEG`, `RAD`, `METERS` or `NORMALIZED`. Report what
  your driver actually returns, since nothing converts for you.
</ParamField>

<ParamField path="space" type="Space" default="POSITION">
  What a number means when you send it.

  | Space | A value of `1.5` means | Typical machine |
  | - | - | - |
  | `POSITION` | go to 1.5 and stay there | an arm, a gripper |
  | `VELOCITY` | keep moving at 1.5 until told otherwise | wheels, a drone |
  | `EFFORT` | push with 1.5 of force | a gripper holding something soft |

  The difference matters when commands stop. A `POSITION` joint is already where
  it was told to be, while a `VELOCITY` one keeps going.
</ParamField>

<ParamField path="control_hz" type="int" default="50">
  The rate this machine expects to run at. Advertised to the other end.
</ParamField>

<ParamField path="capabilities" type="tuple[str, ...]" default="()">
  A list of what else this machine measures. Add `"load"` if your `telemetry()`
  reports motor load, `"temp_c"` if it reports temperature.

  It is a label for your own use. Leave it empty if you are not reading it
  anywhere.
</ParamField>

<Warning>
  Names, order, units and space are hashed into a schema both ends compare. If
  they differ, the leader refuses to command. Keep `JOINTS` and the units in one
  module that both machines import.

  Once a session is running, a descriptor the follower announces with a
  different hash is adopted with a logged warning rather than refused. The
  check that stops you is the one at control time.
</Warning>

## Machines that mix units

Some machines cannot describe every joint the same way. A mobile manipulator
drives its wheels in metres per second and its arm in radians, so one set of
units will not do.

Put each set of joints in its own `Group`, and give the descriptor both.

| Machine | Needs |
| - | - |
| An arm | one group, everything in degrees |
| A mobile base | one group, everything in metres per second |
| A mobile manipulator | two groups, wheels and arm |
| A calibrated SO-101 | two groups, the body in degrees and the gripper on `0…100` |

Groups change nothing about the code you write. `read_joints()` and
`write_joints()` still handle one dict for the whole machine, so give every
joint a different name.

## Examples

### An arm

One group, one unit. Nothing here needs `Group` at all.

```python theme={null}
from zeroruntime.teleops import single_group_descriptor, Units

JOINTS = ("shoulder_pan", "shoulder_lift", "elbow_flex",
          "wrist_flex", "wrist_roll", "gripper")

def descriptor(self):
    return single_group_descriptor("my-arm", "so101", JOINTS,
                                   units=Units.DEG, control_hz=50)
```

### A mobile base with an arm

Wheels are driven at a speed, the arm is sent to a position, so they cannot
share a group.

```python theme={null}
from zeroruntime.teleops import RobotDescriptor, Group, Space, Units

BASE = ("left_wheel", "right_wheel")        # metres per second
ARM = ("shoulder", "elbow", "wrist")        # radians

def descriptor(self):
    return RobotDescriptor(
        robot_id="my-base", profile="mobile-manipulator", control_hz=50,
        groups=(Group("base", BASE, Space.VELOCITY, Units.METERS),
                Group("arm", ARM, Space.POSITION, Units.RAD)))

def read_joints(self):
    return {**self.base.wheel_speeds(), **self.arm.positions()}
```

<Note>
  Set `slew` per joint on a machine like this, since one number would mean
  `0.05` metres per second on the wheels and `0.05` radians on the arm at the
  same time.

  ```python theme={null}
  safety=teleop.SafetyConfig(slew={
      "left_wheel": 0.05, "right_wheel": 0.05,        # m/s per tick
      "shoulder": 0.02, "elbow": 0.02, "wrist": 0.02  # rad per tick
  })
  ```
</Note>

### A drone

Four stick axes, all normalised to `-1…1`, which is one group.

```python theme={null}
from zeroruntime.teleops import single_group_descriptor, Space, Units

AXES = ("throttle", "roll", "pitch", "yaw")

def descriptor(self):
    return single_group_descriptor("my-drone", "quadcopter", AXES,
                                   space=Space.VELOCITY,
                                   units=Units.NORMALIZED,
                                   control_hz=50)
```

<Note>
  A drone reports speed, not position, so
  [`max_misalignment`](/robotics/api-reference/python/leader#starting-to-drive) does not
  apply. Leave it unset.
</Note>

## Related

<CardGroup cols={2}>
  <Card title="Follower" icon="robot" href="/robotics/api-reference/python/follower">
    Where descriptor() is implemented, and when it is called.
  </Card>

  <Card title="SafetyConfig" icon="sliders" href="/robotics/api-reference/python/safety-config">
    Limits and speed caps, all of them in the units declared here.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.