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.
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 example does.
str
required
A name for this machine. The two ends use different ids, such as my-arm and my-controller.
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.
tuple[str, ...]
required
The joint names, in order. Order is part of the schema, so both ends must list them identically.
Units
default:"DEG"
What the numbers mean: DEG, RAD, METERS or NORMALIZED. Report what your driver actually returns, since nothing converts for you.
Space
default:"POSITION"
What a number means when you send it.The difference matters when commands stop. A POSITION joint is already where it was told to be, while a VELOCITY one keeps going.
int
default:"50"
The rate this machine expects to run at. Advertised to the other end.
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.
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.

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. 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.

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.
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.

A drone

Four stick axes, all normalised to -1…1, which is one group.
A drone reports speed, not position, so max_misalignment does not apply. Leave it unset.

Follower

Where descriptor() is implemented, and when it is called.

SafetyConfig

Limits and speed caps, all of them in the units declared here.