ENPIRE is a reset-execute-verify-refine harness for code-as-policy, robot tools, and real-world RL. It’s built around a bimanual YAM station, and I don’t have one. I do have clanker, a 3D-printed SO-101, so I ported the arm-level stack over to it.

clanker, a 3D-printed SO-101 arm.
clanker.

The two arms are pretty different:

  YAM SO-101
Arm joints 6 5
Motors DaMiao, impedance control Feetech STS3215, position control
Gains kp/kd per command Ignored, the servos run their own position loop
Force estimate, gravity comp Yes No
Arms per station 2 1

The goal was for the rest of ENPIRE to see an SO-101 as just another arm, without changing any of the YAM code.

1. Kinematics

First I needed a model of the arm. TheRobotStudio’s SO-ARM100 repo has an MJCF for the SO-101, so I vendored the kinematic tree from so101_new_calib.xml, removed the meshes, and renamed the gripper frame to gripper_site. I follow LeRobot’s degrees calibration, where each joint reads zero at the middle of its range.

For inverse kinematics I used mink, which solves a small QP each step for joint velocities that move the end-effector toward a target frame, subject to joint/workspace constraints.

Unfortunately the SO-101 only has 5 arm joints, and 5 < 6, so some pose targets are unreachable. If you weight position and orientation equally, the solver compromises on both and you end up somewhere you didn’t ask for. So the IK weights position at 1.0 and orientation at 0.1, supports a position-only solve, and returns the residual position and orientation errors. That way the caller can check whether the solution is actually good before commanding it.

2. Robot profile

ENPIRE describes each robot with a RobotProfile: joint limits, home pose, gripper range, end-effector frame, control frequency, and cameras. so101_profile fills this in for a single arm at 30 Hz with no cameras. It also sets interpolation gains, but those only shape tracking in simulation and the real servos ignore them.

3. Driver

LeRobot already has a Feetech driver for the SO-101, so I wrap it instead of talking to the servos directly. Then I converted from degrees to radians.

So101Robot exposes the same methods as YamRobot. It accepts kp and kd but ignores them, since the STS3215 servos only run their internal position loop.

4. Arm server

On a YAM station each arm runs behind a portal RPC server, and the rest of ENPIRE talks to it through FollowerRobotClient. I wrote an SO-101 arm server that binds the same RPC names, so FollowerRobotClient drives an SO-101 with no changes. The follower runs on port 11337.

5. Environment

RealSo101Env covers the arm API of RealYamEnv. Observations come back as joint positions, gripper position, and the end-effector pose from forward kinematics. Beyond raw joint commands it has move_to_joint_pos, which linearly interpolates to a target under a max joint velocity, plus go_home and set_gripper.

Next steps

This only covers the arm. Cameras, grasp planning, cuRobo, and the real-world RL stack all still assume a YAM station, so those come next. After that I want to run more ENPIRE tasks on clanker.