Koch 1.1

The lerobot docs keep changing, which is super annoying. Hopefully am calibrating this for the last time.

Issues:

  • right follower: the elbow flex is fcked up, and the shoulder pan is fucked up

TODO: I need to pin the port to a particular arm, so that every time it unplugs, its the same.

Motors

The calibration was a little confusing, and how the motors are actually encoded.

The motors are in extended mode, which means that there’s no wrap around, if you turn the motor mutliple times, it will remember (-inf to inf range)

for motor in self.bus.motors:
    self.bus.write("Operating_Mode", motor, OperatingMode.EXTENDED_POSITION.value)

HOWEVER, Each motor has a reading from 0 to 4095 why you first boot it. Then, we will use the calibration file to apply this transformation:

Present_Position = Actual_Position + Homing_Offset

The Homing_offset gets stored to non-volatile memory so it remembers it. Hwoever, the actual_position is always measured beteween 0 and 4095. Now imagine your actual value is 50, and the next time its at -50 (+ 4095 = 4045). So the present position can have HUGE jumps if your actual position is close to 0/4095 boundary. This happened to me with the wrist roll :( The trick is to add a function to check this at init time, and you expect the calibrated value to be between 0 and 4095 (because that is what you set during the calibration).

This is an entry in the calibration file:

    "wrist_roll": {
        "id": 5,
        "drive_mode": 0,
        "homing_offset": -1975,
        "range_min": 0,
        "range_max": 4095
    },
  • The homing offset will be set such that the mid point is actually the midpoint, so you don’t suffer from underflow/overflow afterwards

The range_min and range_max are used to set the midpoint.

Ports

Finding the ports

lerobot-find-port

These are the ports when I plug the usb adapter into the top place.

Follower left

/dev/tty.usbmodem14201

Follower right

/dev/tty.usbmodem1301

Leader left

/dev/tty.usbmodem1201

Leader right

/dev/tty.usbmodem1101

Add these to your ~/.bashrc

export FOLLOWER_LEFT_PORT=/dev/tty.usbmodem14201
export FOLLOWER_RIGHT_PORT=/dev/tty.usbmodem1301
export LEADER_LEFT_PORT=/dev/tty.usbmodem1201
export LEADER_RIGHT_PORT=/dev/tty.usbmodem1101
export TOP_CAMERA_INDEX_OR_PATH=0
export FRONT_CAMERA_INDEX_OR_PATH=1

On steven-beast, setup udev rules like this https://chatgpt.com/share/68d1dd62-132c-8002-a314-b97bacf3e86c

/dev/ttyACM1
/dev/ttyACM0
/dev/ttyACM3
/dev/ttyACM2

Also for cameras https://chatgpt.com/share/68d2050a-30c0-8002-a55d-ee417181d3e6

ls -l /dev/serial/by-path/
total 0
lrwxrwxrwx 1 root root 13 Sep 22 19:25 pci-0000:0c:00.3-usb-0:1.1:1.0 -> ../../ttyACM2
lrwxrwxrwx 1 root root 13 Sep 22 19:25 pci-0000:0c:00.3-usb-0:1.2:1.0 -> ../../ttyACM3
lrwxrwxrwx 1 root root 13 Sep 22 19:24 pci-0000:0c:00.3-usb-0:1.3:1.0 -> ../../ttyACM0
lrwxrwxrwx 1 root root 13 Sep 22 19:24 pci-0000:0c:00.3-usb-0:1.4.2:1.0 -> ../../ttyACM1
lrwxrwxrwx 1 root root 13 Sep 22 19:25 pci-0000:0c:00.3-usbv2-0:1.1:1.0 -> ../../ttyACM2
lrwxrwxrwx 1 root root 13 Sep 22 19:25 pci-0000:0c:00.3-usbv2-0:1.2:1.0 -> ../../ttyACM3
lrwxrwxrwx 1 root root 13 Sep 22 19:24 pci-0000:0c:00.3-usbv2-0:1.3:1.0 -> ../../ttyACM0
lrwxrwxrwx 1 root root 13 Sep 22 19:24 pci-0000:0c:00.3-usbv2-0:1.4.2:1.0 -> ../../ttyACM1
export FOLLOWER_LEFT_PORT=/dev/follower-left
export FOLLOWER_RIGHT_PORT=/dev/follower-right
export LEADER_LEFT_PORT=/dev/leader-left
export LEADER_RIGHT_PORT=/dev/leader-right
export TOP_CAMERA_INDEX_OR_PATH=/dev/camera-top
export FRONT_CAMERA_INDEX_OR_PATH=/dev/camera-front

Calibrate

lerobot-calibrate \
    --robot.type=bi_koch_follower \
    --robot.left_arm_port=$FOLLOWER_LEFT_PORT \
    --robot.right_arm_port=$FOLLOWER_RIGHT_PORT \
    --robot.id=bimanual_follower
lerobot-calibrate \
    --teleop.type=bi_koch_leader \
    --robot.left_arm_port=$FOLLOWER_LEFT_PORT \
    --robot.right_arm_port=$FOLLOWER_RIGHT_PORT \
    --teleop.id=bimanual_leader

Teleop

http://steven-beast:9090/?url=rerun%2Bhttp%3A%2F%2Fsteven-beast%3A9876%2Fproxy

Single arm

lerobot-teleoperate \
    --robot.type=koch_follower \
    --robot.port=$FOLLOWER_LEFT_PORT \
    --robot.id=follower \
    --teleop.type=koch_leader \
    --teleop.port=$LEADER_LEFT_PORT \
    --teleop.id=leader \
    --display_data=true
lerobot-teleoperate \
    --robot.type=bi_koch_follower \
    --robot.left_arm_port=$FOLLOWER_LEFT_PORT \
    --robot.right_arm_port=$FOLLOWER_RIGHT_PORT \
    --robot.id=bimanual_follower \
    --robot.cameras="{ top: {type: opencv, index_or_path: $TOP_CAMERA_INDEX_OR_PATH, width: 640, height: 360, fps: 30}, left_wrist: {type: opencv, index_or_path: $LEFT_WRIST_CAMERA_INDEX_OR_PATH, width: 640, height: 480, fps: 30}, right_wrist: {type: opencv, index_or_path: $RIGHT_WRIST_CAMERA_INDEX_OR_PATH, width: 640, height: 480, fps: 30} }" \
    --teleop.type=bi_koch_leader \
    --teleop.left_arm_port=$LEADER_LEFT_PORT \
    --teleop.right_arm_port=$LEADER_RIGHT_PORT \
    --teleop.id=bimanual_leader \
    --display_data=true

Record a dataset

Keyboard controls:

  • esc to stop data collection
  • right arrow to terminate episode early
  • Left arrow to rerecord episode
lerobot-record \
    --robot.type=bi_koch_follower \
    --robot.left_arm_port=$FOLLOWER_LEFT_PORT \
    --robot.right_arm_port=$FOLLOWER_RIGHT_PORT \
    --robot.id=bimanual_follower \
    --teleop.type=bi_koch_leader \
    --teleop.left_arm_port=$LEADER_LEFT_PORT \
    --teleop.right_arm_port=$LEADER_RIGHT_PORT \
    --teleop.id=bimanual_leader \
    --robot.cameras="{ top: {type: opencv, index_or_path: $TOP_CAMERA_INDEX_OR_PATH, width: 640, height: 480, fps: 30}, left_wrist: {type: opencv, index_or_path: $LEFT_WRIST_CAMERA_INDEX_OR_PATH, width: 640, height: 480, fps: 30}, right_wrist: {type: opencv, index_or_path: $RIGHT_WRIST_CAMERA_INDEX_OR_PATH, width: 640, height: 480, fps: 30} }" \
      --display_data=True \
      --dataset.repo_id=${HF_USER}/koch-tshirt-pickplace \
      --dataset.num_episodes=20 \
      --dataset.reset_time_s=15 \
      --dataset.episode_time_s=30 \
      --dataset.single_task="Pick up the t-shirt and put it in the bin" \
      --dataset.video_encoding_batch_size=20 \
      --dataset.num_image_writer_processes=16
  • TODO: there’s actually a flag to bulk render the videos, else its actually really time consuming to do this rendering on each video…
      --dataset.single_task="Fold the t-shirt and put it in the bin" \

Visualize your dataset

You can go to this url and paste in the url, ex

Gongsta/koch-baby-tshirt-folding

https://huggingface.co/spaces/lerobot/visualize_dataset

Replay an episode!

This is a really helpful feature, can help validate the data integrity. It’s also kind of really cool :))

HF_USER=Gongsta lerobot-replay \
    --robot.type=bi_koch_follower \
    --robot.left_arm_port=$FOLLOWER_LEFT_PORT \
    --robot.right_arm_port=$FOLLOWER_RIGHT_PORT \
    --robot.id=bimanual_follower \
    --dataset.repo_id=${HF_USER}/koch-baby-tshirt-folding \
    --dataset.episode=20

Training

Run this on your RTX5090.

SmolVLA finetuning

export HF_USER=Gongsta
export OUTPUT_DIR=outputs/train/tshirt_smolvla
lerobot-train \
  --policy.path=lerobot/smolvla_base \
  --policy.repo_id=${HF_USER}/smolvla-hf-policy-koch-baby-tshirt-folding \
  --policy.device=cuda \
  --dataset.repo_id=${HF_USER}/koch-baby-tshirt-folding \
  --dataset.video_backend=pyav \
  --batch_size=128 \
  --steps=20000 \
  --save_freq=2000 \
  --output_dir=$OUTPUT_DIR \
  --job_name=tshirt_smolvla_training \
  --num_workers=12 \
  --wandb.enable=true

pi0

export HF_USER=Gongsta
export OUTPUT_DIR=outputs/train/tshirt_pi0_hf
lerobot-train \
  --dataset.repo_id=${HF_USER}/koch-baby-tshirt-folding \
  --dataset.video_backend=pyav \
  --policy.type=pi0 \
  --batch_size=4 \
  --steps=20000 \
  --save_freq=2000 \
  --output_dir=$OUTPUT_DIR \
  --job_name=tshirt-pi0-hf-training \
  --policy.device=cuda \
  --wandb.enable=true \
  --num_workers=12 \
  --policy.repo_id=${HF_USER}/pi0-hf-policy-koch-baby-tshirt-folding

Also see Openpi for using the openpi codebase.

Policy Inference

HF_USER=Gongsta lerobot-record \
    --robot.type=bi_koch_follower \
    --robot.left_arm_port=$FOLLOWER_LEFT_PORT \
    --robot.right_arm_port=$FOLLOWER_RIGHT_PORT \
    --robot.id=bimanual_follower \
    --robot.cameras="{ top: {type: opencv, index_or_path: $TOP_CAMERA_INDEX_OR_PATH, width: 640, height: 480, fps: 30}, left_wrist: {type: opencv, index_or_path: $LEFT_WRIST_CAMERA_INDEX_OR_PATH, width: 640, height: 480, fps: 30}, right_wrist: {type: opencv, index_or_path: $RIGHT_WRIST_CAMERA_INDEX_OR_PATH, width: 640, height: 480, fps: 30} }" \
    --dataset.repo_id=${HF_USER}/eval_$(date +%Y-%m-%d_%H-%M-%S) \
    --dataset.single_task="Fold the t-shirt and put it in the bin" \
      --display_data=true \
    --policy.path=${HF_USER}/smolvla-hf-policy-koch-baby-tshirt-folding \
    --dataset.push_to_hub=False
export HF_USER=Gongsta
lerobot-record \
    --robot.type=bi_koch_follower \
    --robot.left_arm_port=$FOLLOWER_LEFT_PORT \
    --robot.right_arm_port=$FOLLOWER_RIGHT_PORT \
    --robot.id=bimanual_follower \
    --robot.cameras="{ top: {type: opencv, index_or_path: $TOP_CAMERA_INDEX_OR_PATH, width: 640, height: 480, fps: 30}, left_wrist: {type: opencv, index_or_path: $LEFT_WRIST_CAMERA_INDEX_OR_PATH, width: 640, height: 480, fps: 30}, right_wrist: {type: opencv, index_or_path: $RIGHT_WRIST_CAMERA_INDEX_OR_PATH, width: 640, height: 480, fps: 30} }" \
    --dataset.repo_id=${HF_USER}/eval_$(date +%Y-%m-%d_%H-%M-%S) \
    --dataset.single_task="Fold the t-shirt and put it in the bin" \
      --display_data=true \
    --policy.path=Gongsta/pi0-hf-policy-koch-baby-tshirt-folding \
    --dataset.push_to_hub=False

Remote inference (NOT OWRKING)

python src/lerobot/scripts/server/robot_client.py \
    --server_address=localhost:8080 \
    --robot.type=bi_koch_follower \
    --robot.left_arm_port=$FOLLOWER_LEFT_PORT \
    --robot.right_arm_port=$FOLLOWER_RIGHT_PORT \
    --robot.id=bimanual_follower \
    --robot.cameras="{ top: {type: opencv, index_or_path: $TOP_CAMERA_INDEX_OR_PATH, width: 640, height: 480, fps: 30}, left_wrist: {type: opencv, index_or_path: $LEFT_WRIST_CAMERA_INDEX_OR_PATH, width: 640, height: 480, fps: 30}, right_wrist: {type: opencv, index_or_path: $RIGHT_WRIST_CAMERA_INDEX_OR_PATH, width: 640, height: 480, fps: 30} }" \
    --task="Fold the t-shirt and put it in the bin" \
    --policy_type=pi0 \
    --pretrained_name_or_path=Gongsta/pi0-hf-policy-koch-baby-tshirt-folding \
    --policy_device=cuda \
    --actions_per_chunk=50 \
    --chunk_size_threshold=0.5 \
    --aggregate_fn_name=weighted_average \
    --debug_visualize_queue_size=True

Remote inference with my implementation on OpenPi

Server

uv run scripts/serve_policy.py policy:checkpoint --policy.config=pi05_koch --policy.dir=/home/steven/research/openpi/checkpoints/pi05_koch/my_experiment/10000/

Client

python src/lerobot/async_inference/robot_openpi_client.py \
    --server_address=localhost:8000 \
    --robot.type=bi_koch_follower \
    --robot.left_arm_port=$FOLLOWER_LEFT_PORT \
    --robot.right_arm_port=$FOLLOWER_RIGHT_PORT \
    --robot.id=bimanual_follower \
    --robot.cameras="{ top: {type: opencv, index_or_path: $TOP_CAMERA_INDEX_OR_PATH, width: 640, height: 480, fps: 30}, left_wrist: {type: opencv, index_or_path: $LEFT_WRIST_CAMERA_INDEX_OR_PATH, width: 640, height: 480, fps: 30}, right_wrist: {type: opencv, index_or_path: $RIGHT_WRIST_CAMERA_INDEX_OR_PATH, width: 640, height: 480, fps: 30} }" \
    --task="Fold the t-shirt and put it in the bin" \
    --actions_per_chunk=20

Debugging Issues

Power surges

Use a max_relative_target= 1.0 / (try 2.0 or more). Maybe set this as a default?

Cuda 12.9

Running with RTX5090

I had to reinstall pytorch with pip install --force-reinstall to support CUDA12.9. And then I had issue with the video encoding, which i fixed via --dataset.video_backend=pyav.

  • The pyproject.toml needs to be upgraded
pip3 install --force-reinstall torch torchvision --index-url https://download.pytorch.org/whl/cu129

Transformer version too low

https://github.com/huggingface/lerobot/issues/1406

  • Transformers version is too low for running pi0 training / inference
AttributeError: 'GemmaForCausalLM' object has no attribute 'embed_tokens'

Fix:

pip install --upgrade transformers

Keyboard controls not working

You need to give permissions to your terminal / Cursor in Accessibility (if I recall correctly).

mean

Got this at inference time. The issue is that they use normalization for the dataset

assert not torch.isinf(mean).any(), _no_stats_error_str("mean")
AssertionError: mean is infinity. You should either initialize with stats as an argument, or use a pretrained model.

The urdf

<?xml version="1.0" encoding="utf-8"?>
<!-- Combined URDF file with motor links as children of original links -->
<robot
  name="follower">
    <mujoco>
    <compiler meshdir="./meshes"/>
  </mujoco>
  <!-- Original base_link -->
  <link name="base_link">
    <inertial>
      <origin
        xyz="0.034538 0.0021165 0.013963"
        rpy="0 0 0" />
      <mass
        value="0.067266" />
      <inertia
        ixx="4.4766E-05"
        ixy="-2.3205E-06"
        ixz="4.3559E-07"
        iyy="2.1965E-05"
        iyz="1.479E-07"
        izz="5.9106E-05" />
    </inertial>
    <visual>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/base_link.STL" />
      </geometry>
      <material name="">
        <color rgba="0.79216 0.81961 0.93333 1" />
      </material>
    </visual>
    <visual>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/base_link_motor.STL" />
      </geometry>
      <material name="">
        <color rgba="0.79216 0.81961 0.93333 1" />
      </material>
    </visual>
    <collision>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/base_link.STL" />
      </geometry>
    </collision>
    <collision>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/base_link_motor.STL" />
      </geometry>
    </collision>
  </link>

  <!-- Original link_1 -->
  <link name="link_1">
    <inertial>
      <origin
        xyz="0.011924 -0.00048792 0.013381"
        rpy="0 0 0" />
      <mass
        value="0.05014" />
      <inertia
        ixx="7.6045E-06"
        ixy="-2.4893E-07"
        ixz="-2.8338E-08"
        iyy="1.2358E-05"
        iyz="-5.112E-09"
        izz="1.4492E-05" />
    </inertial>
    <visual>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_1.STL" />
      </geometry>
      <material name="">
        <color rgba="0.79216 0.81961 0.93333 1" />
      </material>
    </visual>
    <visual>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_1_motor.STL" />
      </geometry>
      <material name="">
        <color rgba="0.79216 0.81961 0.93333 1" />
      </material>
    </visual>
    <collision>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_1.STL" />
      </geometry>
    </collision>
    <collision>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_1_motor.STL" />
      </geometry>
    </collision>
  </link>

  <joint name="joint_1" type="revolute">
    <origin xyz="0.012 0 0.0409" rpy="0 0 0" />
    <parent link="base_link" />
    <child link="link_1" />
    <axis xyz="0 0 -1" />
    <limit lower="-3.14" upper="3.14" effort="0.5" velocity="100" />
  </joint>

  <!-- Original link_2 -->
  <link name="link_2">
    <inertial>
      <origin
        xyz="0.0011747 0.02097 0.071547"
        rpy="0 0 0" />
      <mass
        value="0.050177" />
      <inertia
        ixx="3.7018E-05"
        ixy="2.5963E-09"
        ixz="-2.8959E-06"
        iyy="3.3772E-05"
        iyz="-1.6011E-10"
        izz="8.2375E-06" />
    </inertial>
    <visual>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_2.STL" />
      </geometry>
      <material name="">
        <color rgba="0.79216 0.81961 0.93333 1" />
      </material>
    </visual>
    <visual>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_2_motor.STL" />
      </geometry>
      <material name="">
        <color rgba="0.79216 0.81961 0.93333 1" />
      </material>
    </visual>
    <collision>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_2.STL" />
      </geometry>
    </collision>
    <collision>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_2_motor.STL" />
      </geometry>
    </collision>
  </link>

  <joint name="joint_2" type="revolute">
    <origin xyz="0 -0.0209 0.0154" rpy="0 0 0" />
    <parent link="link_1" />
    <child link="link_2" />
    <axis xyz="0 1 0" />
    <limit lower="-3.14" upper="3.14" effort="0.5" velocity="100" />
  </joint>

  <!-- Original link_3 -->
  <link name="link_3">
    <inertial>
      <origin
        xyz="-0.05537 0.014505 0.0028659"
        rpy="0 0 0" />
      <mass
        value="0.06379" />
      <inertia
        ixx="7.3427E-06"
        ixy="2.6058E-09"
        ixz="1.895E-07"
        iyy="2.2231E-05"
        iyz="9.8075E-11"
        izz="2.4506E-05" />
    </inertial>
    <visual>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_3.STL" />
      </geometry>
      <material name="">
        <color rgba="0.79216 0.81961 0.93333 1" />
      </material>
    </visual>
    <visual>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_3_motor.STL" />
      </geometry>
      <material name="">
        <color rgba="0.79216 0.81961 0.93333 1" />
      </material>
    </visual>
    <collision>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_3.STL" />
      </geometry>
    </collision>
    <collision>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_3_motor.STL" />
      </geometry>
    </collision>
  </link>

  <joint name="joint_3" type="revolute">
    <origin xyz="-0.0148 0.0065 0.1083" rpy="0 0 0" />
    <parent link="link_2" />
    <child link="link_3" />
    <axis xyz="0 1 0" />
    <limit lower="-3.14" upper="3.14" effort="0.5" velocity="100" />
  </joint>

  <!-- Original link_4 -->
  <link name="link_4">
    <inertial>
      <origin
        xyz="-0.02652 0.019195 -9.0614E-06"
        rpy="0 0 0" />
      <mass
        value="0.019805" />
      <inertia
        ixx="2.9577E-06"
        ixy="2.8302E-08"
        ixz="-1.331E-10"
        iyy="1.0783E-06"
        iyz="1.3763E-09"
        izz="2.8759E-06" />
    </inertial>
    <visual>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_4.STL" />
      </geometry>
      <material name="">
        <color rgba="0.79216 0.81961 0.93333 1" />
      </material>
    </visual>
    <visual>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_4_motor.STL" />
      </geometry>
      <material name="">
        <color rgba="0.79216 0.81961 0.93333 1" />
      </material>
    </visual>
    <collision>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_4.STL" />
      </geometry>
    </collision>
    <collision>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_4_motor.STL" />
      </geometry>
    </collision>
  </link>

  <joint name="joint_4" type="revolute">
    <origin xyz="-0.10048 5E-05 0.0026999" rpy="0 0 0" />
    <parent link="link_3" />
    <child link="link_4" />
    <axis xyz="0 1 0" />
    <limit lower="-3.14" upper="3.14" effort="0.5" velocity="100" />
  </joint>

  <!-- Original link_5 -->
  <link name="link_5">
    <inertial>
      <origin
        xyz="-0.019091 0.0053379 0.00018011"
        rpy="0 0 0" />
      <mass
        value="0.029277" />
      <inertia
        ixx="3.5905E-06"
        ixy="1.0601E-06"
        ixz="4.6179E-08"
        iyy="6.8334E-06"
        iyz="-2.8448E-09"
        izz="8.1125E-06" />
    </inertial>
    <visual>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_5.STL" />
      </geometry>
      <material name="">
        <color rgba="0.79216 0.81961 0.93333 1" />
      </material>
    </visual>
    <visual>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_5_motor.STL" />
      </geometry>
      <material name="">
        <color rgba="0.79216 0.81961 0.93333 1" />
      </material>
    </visual>
    <collision>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_5.STL" />
      </geometry>
    </collision>
    <collision>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_5_motor.STL" />
      </geometry>
    </collision>
  </link>

  <joint name="joint_5" type="revolute">
    <origin xyz="-0.045 0.013097 0" rpy="0 0 0" />
    <parent link="link_4" />
    <child link="link_5" />
    <axis xyz="1 0 0" />
    <limit lower="-3.14" upper="3.14" effort="0.5" velocity="100" />
  </joint>

  <!-- Original link_6 -->
  <link name="link_6">
    <inertial>
      <origin
        xyz="-0.02507 0.0010817 -0.01414"
        rpy="0 0 0" />
      <mass
        value="0.012831" />
      <inertia
        ixx="1.5159E-06"
        ixy="3.156E-07"
        ixz="3.409E-08"
        iyy="3.449E-06"
        iyz="-3.8705E-09"
        izz="2.4565E-06" />
    </inertial>
    <visual>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_6.STL" />
      </geometry>
      <material name="">
        <color rgba="0.79216 0.81961 0.93333 1" />
      </material>
    </visual>
    <collision>
      <origin xyz="0 0 0" rpy="0 0 0" />
      <geometry>
        <mesh filename="package://follower/meshes/link_6.STL" />
      </geometry>
    </collision>
  </link>

  <joint name="joint_6" type="revolute">
    <origin xyz="-0.01315 -0.0075 0.0145" rpy="0 0 0" />
    <parent link="link_5" />
    <child link="link_6" />
    <axis xyz="0 0 -1" />
    <limit lower="-2.45" upper="0.032" effort="0.5" velocity="100" />
  </joint>
</robot>