Skip to the content.

Chapter 07 — Joints in a Row

This chapter is about order. When the humanoid’s brain hands back a chunk of numbers, everything depends on which number drives which joint — and both sides must agree, exactly, with no room for interpretation. It assumes you know what a degree of freedom is and that actions cross the wire as flat lists of numbers.


A list of numbers is not self-describing

Chapter 5 showed the humanoid’s reply: actions_flat, a flat list of 1160 numbers, meant to be read as 40 actions of 29 numbers each. Take one action — 29 numbers. Which one is the left knee? Which is the waist? Which is the right shoulder?

The list does not say. It is just numbers in a row. The meaning is entirely a matter of a convention both sides have agreed on in advance: the numbers are in the robot’s joint order, and everybody uses the same joint order. Get that convention right and the robot moves correctly. Get it off by one — or scramble two joints — and the robot does something between subtly wrong and violently broken, with, once again, no error message.

So what fixes the order? The answer is refreshingly boring, and that is the point:

Insight: URDF document order is the single source of truth. The order of joints is the order they appear in the robot’s URDF blueprint file — top to bottom, “document order.” The Python server reads the URDF and lists its joints in that order. The visualization tool reads the same URDF and gets the same order. The Unity scene resolves each joint by its URDF name and arranges them in that same order. Three programs, one file, one order. Nobody hand-numbers joints; they all defer to the blueprint. That deferral is what makes an unlabeled list of numbers safe to send.


The G1’s 29 joints, in order

Concretely, here is the order every part of the system uses for the 29-DoF Unitree G1. It is worth seeing in full, because the block structure explains a lot of later code:

Index Joints Block
0–5 left_hip_pitch, left_hip_roll, left_hip_yaw, left_knee, left_ankle_pitch, left_ankle_roll left leg (6)
6–11 right_hip_pitch … right_ankle_roll right leg (6)
12–14 waist_yaw, waist_roll, waist_pitch waist (3)
15–21 left_shoulder_pitch, left_shoulder_roll, left_shoulder_yaw, left_elbow, left_wrist_roll, left_wrist_pitch, left_wrist_yaw left arm (7)
22–28 right_shoulder_pitch … right_wrist_yaw right arm (7)

Legs first, then waist, then arms — that is the blueprint’s order, so it is everyone’s order. Now several earlier details click into place. Chapter 3 said the humanoid’s legs are held while the arms move: that is indices 0–11 held, 12–28 driven. The structured-observation code that builds the model’s input knows the waist starts at index 12, the left arm at 15, the right arm at 22 — those are not magic numbers, they are the block boundaries of this table. When the model returns arm targets, the mapping code writes them into exactly those slots.

Because both sides derive this order from the same file, they cannot drift apart by accident. If someone swaps to a different URDF, both sides pick up the new order together. The contract is the file, not a comment in the code that someone has to remember to update.


The trap: mimic joints

Now the subtlety that cost real debugging, and the reason “count the degrees of freedom” is not as simple as it sounds. Some robot joints are not independently controlled — they are mimic joints, mechanically coupled to move in lockstep with another joint.

Mimic joint — a joint whose motion is a fixed function of another joint’s, not independently commanded. A robot finger built so that curling the knuckle automatically curls the fingertip has a mimic joint: one motor, two moving parts. In the URDF it is marked <mimic joint="..." multiplier="..." offset="..."/>, meaning my angle = that joint’s angle × multiplier + offset.

Mimic joints exist in the blueprint (the visualization needs them to draw the finger correctly), but they are not actuated — you do not send them a target, because they follow their source automatically. So the action array must contain only the actuated joints, and the mapping everywhere keys off the list of joints that are movable and not mimic, in document order.

This is where the counting gets treacherous. Consider the hand-equipped humanoids:

The action array for the GR-1 has 44 entries, not 54. And because the mimic joints are removed from the actuated list, the surviving hand joints become contiguous in a way the raw blueprint would not suggest — the six actuated finger degrees of freedom per hand end up in one unbroken block. Any code that assumed the raw 54-joint numbering, or that forgot to drop the mimic joints, would land every hand command on the wrong finger.

Insight: the actuated count, not the joint count, is the contract. “The GR-1 has 54 joints” and “the GR-1’s action array has 44 numbers” are both true and describe the same robot. The action space is the actuated joints — movable and not mimic — in document order. Every consumer (server, Unity, visualization) must compute that same filtered list from the same URDF, or the whole hand is scrambled. This is precisely the kind of off-by-a-few that produces a robot moving almost right, which is the hardest failure to diagnose.

The Unitree G1’s Dex3 hand, by contrast, has no mimic coupling — its seven finger degrees of freedom are all independently actuated — so the G1+Dex3 is “full fidelity”: every joint the blueprint draws is a joint you command. That is one reason it is the preferred hand-equipped robot in the project (more in Chapters 15 and 17).


What you now understand

Directions (Chapter 6) and joint order (this chapter) are two of the three conventions the messages assume. The third is the deepest split of all: the arm is told to move a little, the humanoid is handed forty exact poses at once. That is the next chapter.

Continue to Chapter 08 — Deltas, Targets, and the Chunk.


The G1 joint order is defined once (as G1Joints in DevVLA/Assets/Editor/VLASceneSetup.cs) and mirrored by the server, which derives its joint names from the same URDF. The actuated-vs-mimic filtering lives in server/urdf_rerun.py (actuated_joint_names), and the mimic gotcha — GR-1 54→44, H1 45→33 — is documented in the repository’s CLAUDE.md.