The gripper

The arm was a class you wrote. The gripper is the other way: a module someone else already wrote, installed into the project in one command.

UMI-slim, the parallel gripper this chapter adds

UMI-slim is a two-finger gripper. One servo drives both fingers, and you command it by how far apart the fingertips are, in millimetres. 0 is closed. About 85 is open. The Feetech variant is UmiSlimFeetechModule.

Install it

From the project root:

solarian install umi-slim

That installs solarian-umi-slim (and the Feetech driver it depends on, if you skipped it) and records the dependency. This plugin ships the module class and the MuJoCo model inside the package. It does not ship a YAML template, so the config file is still yours to write. The class is what you did not have to write.

Mount it

Add the attribute. The name right_gripper is the module name from here on. In so106/robot.py:

from solarian_umi_slim import MJCF_FEETECH_PATH
from solarian_umi_slim.umi_slim_feetech import UmiSlimFeetechModule

and on the class, under right_arm:

right_gripper = UmiSlimFeetechModule()

The gripper's model has to be bolted to the arm, not dropped on the floor. The arm model has a site named flange. The gripper model has a site of the same name. Mating site to site is the attachment. Add a second ModuleAttachment in the scene's attachments list:

ModuleAttachment(
    module="right_gripper",
    mjcf=MJCF_FEETECH_PATH,
    parent_site="right/flange",
    child_site="flange",
    prefix="right_gripper/",
),

parent_site is a site in the scene after the arm's prefix, so it is right/flange, not flange. child_site is a site in the gripper's own file, before its prefix is applied. MJCF_FEETECH_PATH points at the model inside the installed package. You do not copy meshes for the gripper.

right_arm.py does not change. The arm does not know it has a tool. The scene does.

Config

Add a port to conf/deployment/localhost.yaml. On the real robot the gripper shares the arm's bus; using the same path now means Tutorial 3 does not have to invent a second adapter.

actuator_port_right_gripper: /dev/ttyUSB0

Create conf/modules/right_gripper.yaml:

actuator_port: ${deployment.actuator_port_right_gripper}
frequency: 50
interpolator_type: scalar

actuators:
  gripper:
    id: 7
    port: ${actuator_port}
    mode: range_m100_100
    sim_joint: "right_gripper/left_manifold_to_rail"
    sim_range_min: 0.0
    sim_range_max: 0.0545
    sim_drive_mode: 1

id: 7 keeps the gripper off the arm's 1–6. sim_joint is the one prismatic joint the gripper model actuates; an equality in the model drives the other finger the opposite way, so one command opens both. sim_drive_mode: 1 flips the direction so −100 in normalized space is closed and 100 is open, matching the module. You command millimetres. The module converts.

The filename is right_gripper.yaml because the attribute is right_gripper.

Schema and keys

Add the gripper to the observation and the action, or the class check fails at startup.

In so106/observation.py:

class RightGripperObs(ModuleObs):
    gripper = SensorField(So106Robot.right_gripper.gripper.pos)


class So106Observation(Observation):
    right_arm = RightArmObs()
    right_gripper = RightGripperObs()

In so106/action.py:

class RightGripperAction(ModuleAction):
    gripper = ActuatorField(So106Robot.right_gripper.gripper.pos)


class So106Action(Action):
    right_arm = RightArmAction()
    right_gripper = RightGripperAction()

In the controller's __init__, next to the arm's action type:

self.target.right_gripper.action_type = ActionType.MOVE_J
self.target.right_gripper.gripper.value = 40.0

40 mm is half open, so you can see the fingers. Extend _KEYS:

"[": ("gripper", -5),
"]": ("gripper", 5),

and teach on_key_down to write the gripper field when the joint name is gripper, clamped to 0–85, and the arm field otherwise. The gripper value is millimetres, not the −100–100 range. A compact version of the handler:

@event_handler(EventType.KEY_DOWN)
def on_key_down(self, event: Event):
    step = _KEYS.get(event.data["key"])
    if step is None:
        return
    joint, delta = step
    if joint == "gripper":
        field = self.target.right_gripper.gripper
        field.value = max(0.0, min(85.0, field.value + delta))
        return
    field = self.target.right_arm[joint]
    field.value = max(-100.0, min(100.0, field.value + delta))

Look at it

Restart the robot and the controller. The sim arm should be wearing the gripper. [ closes the fingers, ] opens them. The arm keys from the previous chapter still work.

Next: the camera.

Was this page helpful?