Skip to content

Metal Rod Repackaging (Angled Tray)

This example demonstrates how to use Tzara, the Telekinesis Physical AI Agent, to build a repackaging pipeline. The source tray is tilted roughly 23 degrees around the robot's X axis, so Tzara accounts for the tilt when computing pick poses — keeping the TCP aligned with the tray surface for all grid slots. Parts are picked from the angled tray and dropped (unstructured) into a flat destination box grid.

Demo

UR10e picking aluminum rods from an angled tray and placing into a box grid

The Natural Language Instruction

I have an angled tray where aluminum cylindrical parts sorted in a grid structure. The tray has about 23 degrees rotation around X axis of robot base. First pick pose of the grid on the tray i have measured is [-0.3869, 1.3308, 0.1729, -180, -23, 90], note that this is starting pick on the angled tray (that is why -23 deg Y). I need to pick the objects and place in a box also in a grid format. You can drop the objects instead of placing them. Add logging at each step.

The Generated Code

python
# Repackaging pipeline: UR10e + OnRobot RG6
# Picks aluminum rods from a tray tilted 23 degrees around the robot X axis.
# Row-to-row travel has both a Y component and a Z component due to the tilt.
# Parts are dropped (unstructured) into a flat destination box grid.

import math
from loguru import logger

from telekinesis.synapse.robots.manipulators.universal_robots import UniversalRobotsUR10E
from telekinesis.synapse.tools.parallel_grippers.onrobot import OnRobotRG6

# ============================================================
# Constants
# ============================================================

ROBOT_IP         = "192.168.1.2"
GRIPPER_IP       = "192.168.1.1"
GRIPPER_PROTOCOL = "MODBUS_TCP"

# Tray tilt
TRAY_TILT_DEG = 23.0
TRAY_TILT_RAD = math.radians(TRAY_TILT_DEG)

# Pick grid — tray is tilted, so rows move along both Y and Z in the base frame.
# Measured first pick pose: [x, y, z, rx, ry, rz]
PICK_ORIGIN     = [-0.3869, 1.3308, 0.1729]   # meters
PICK_RPY        = [-180.0, -23.0, 90.0]        # degrees; keeps TCP normal to tray surface

PICK_GRID_ROWS  = 4
PICK_GRID_COLS  = 5
ROW_PITCH       = 0.065    # distance between rows measured along the tray surface (m)
COL_PITCH       = 0.065    # distance between columns along X in the base frame (m)

# Row step projected into base frame (tray slopes away from robot, so Z decreases)
ROW_DY_BASE     =  ROW_PITCH * math.cos(TRAY_TILT_RAD)   # positive Y per row
ROW_DZ_BASE     = -ROW_PITCH * math.sin(TRAY_TILT_RAD)   # negative Z per row (slopes down)

# Drop grid — flat box, no orientation constraint (drop, not structured place)
DROP_ORIGIN     = [0.100, 0.820, 0.195]   # meters, first drop slot
DROP_RPY        = [180.0, 0.0, 90.0]      # TCP pointing down, flat orientation
DROP_GRID_ROWS  = 5
DROP_GRID_COLS  = 4
DROP_ROW_PITCH  = -0.070                  # row spacing along Y (m)
DROP_COL_PITCH  =  0.070                  # column spacing along X (m)

# Z levels (meters)
Z_APPROACH = 0.350   # safe clearance height above both the tray and the box

# Gripper settings (mm / N)
GRIPPER_OPEN_MM  = 65.0
GRIPPER_CLOSE_MM = 14.0
GRIPPER_FORCE_N  = 45.0

# Motion parameters
CART_SPEED  = 0.45   # m/s
CART_ACCEL  = 0.65   # m/s^2

# Home joint pose (degrees)
HOME_JOINT_DEG  = [90.0, -90.0, -90.0, -90.0, 90.0, 0.0]
JOINT_SPEED_DEG = 45.0
JOINT_ACCEL_DEG = 90.0


# ============================================================
# Helper functions
# ============================================================

def generate_pick_poses():
    """
    Generate pick poses for the angled tray.
    Column pitch moves along X only (tray width direction).
    Row pitch moves along Y and Z in the base frame due to the 23-degree tilt.
    """
    poses = []
    for r in range(PICK_GRID_ROWS):
        for c in range(PICK_GRID_COLS):
            x = PICK_ORIGIN[0] + c * COL_PITCH
            y = PICK_ORIGIN[1] + r * ROW_DY_BASE
            z = PICK_ORIGIN[2] + r * ROW_DZ_BASE
            poses.append([x, y, z] + list(PICK_RPY))
    logger.debug(
        f"Generated {len(poses)} pick poses "
        f"(rows={PICK_GRID_ROWS}, cols={PICK_GRID_COLS}, "
        f"dy_per_row={ROW_DY_BASE:.4f} m, dz_per_row={ROW_DZ_BASE:.4f} m)"
    )
    return poses


def generate_drop_poses():
    """Generate drop poses for the flat destination box grid (row-major)."""
    poses = []
    for r in range(DROP_GRID_ROWS):
        for c in range(DROP_GRID_COLS):
            x = DROP_ORIGIN[0] + c * DROP_COL_PITCH
            y = DROP_ORIGIN[1] + r * DROP_ROW_PITCH
            z = DROP_ORIGIN[2]
            poses.append([x, y, z] + list(DROP_RPY))
    logger.debug(f"Generated {len(poses)} drop poses ({DROP_GRID_ROWS} rows x {DROP_GRID_COLS} cols)")
    return poses


def _approach(pose, z_safe):
    """Return a copy of pose with z replaced by z_safe (base-frame clearance height)."""
    p = list(pose)
    p[2] = z_safe
    return p


def pick_part(robot, gripper, pick_pose, index):
    """Execute the pick sequence for a single rod on the angled tray."""
    approach = _approach(pick_pose, Z_APPROACH)

    logger.info(f"[Part {index}] Moving to pick approach")
    logger.debug(f"    approach = {approach}")
    robot.set_cartesian_pose(approach, speed=CART_SPEED, acceleration=CART_ACCEL)
    logger.success(f"[Part {index}] At pick approach")

    logger.info(f"[Part {index}] Opening gripper")
    gripper.move(position=GRIPPER_OPEN_MM, force=GRIPPER_FORCE_N)
    logger.success(f"[Part {index}] Gripper open")

    logger.info(f"[Part {index}] Descending to pick pose on angled tray")
    logger.debug(f"    pick pose = {pick_pose}")
    robot.set_cartesian_pose(pick_pose, speed=CART_SPEED, acceleration=CART_ACCEL)
    logger.success(f"[Part {index}] At pick pose")

    logger.info(f"[Part {index}] Closing gripper to grasp rod")
    gripper.move(position=GRIPPER_CLOSE_MM, force=GRIPPER_FORCE_N)
    logger.success(f"[Part {index}] Rod grasped")

    logger.info(f"[Part {index}] Retreating from tray")
    robot.set_cartesian_pose(approach, speed=CART_SPEED, acceleration=CART_ACCEL)
    logger.success(f"[Part {index}] Retreated from pick")


def drop_part(robot, gripper, drop_pose, index):
    """Move to the drop pose above the box and release the rod."""
    approach = _approach(drop_pose, Z_APPROACH)

    logger.info(f"[Part {index}] Moving to drop approach")
    logger.debug(f"    drop approach = {approach}")
    robot.set_cartesian_pose(approach, speed=CART_SPEED, acceleration=CART_ACCEL)
    logger.success(f"[Part {index}] At drop approach")

    logger.info(f"[Part {index}] Descending to drop pose")
    logger.debug(f"    drop pose = {drop_pose}")
    robot.set_cartesian_pose(drop_pose, speed=CART_SPEED, acceleration=CART_ACCEL)
    logger.success(f"[Part {index}] At drop pose")

    logger.info(f"[Part {index}] Opening gripper to drop rod")
    gripper.move(position=GRIPPER_OPEN_MM, force=GRIPPER_FORCE_N)
    logger.success(f"[Part {index}] Rod dropped into box")

    logger.info(f"[Part {index}] Retreating from drop pose")
    robot.set_cartesian_pose(approach, speed=CART_SPEED, acceleration=CART_ACCEL)
    logger.success(f"[Part {index}] Retreated from drop")


def run_pipeline(robot, gripper):
    """Pick every rod from the angled tray and drop into the flat box grid."""
    pick_poses = generate_pick_poses()
    drop_poses = generate_drop_poses()

    n_parts = min(len(pick_poses), len(drop_poses))
    logger.info(f"Starting repackaging pipeline: {n_parts} rods to transfer")

    logger.info("Moving to home pose before starting")
    robot.set_joint_positions(HOME_JOINT_DEG, speed=JOINT_SPEED_DEG, acceleration=JOINT_ACCEL_DEG)
    logger.success("At home pose")

    for i in range(n_parts):
        logger.info(f"===== Rod {i + 1} / {n_parts} =====")
        pick_part(robot, gripper, pick_poses[i], i + 1)
        drop_part(robot, gripper, drop_poses[i], i + 1)
        logger.success(f"===== Rod {i + 1} complete =====")

    logger.info("All rods transferred — returning to home pose")
    robot.set_joint_positions(HOME_JOINT_DEG, speed=JOINT_SPEED_DEG, acceleration=JOINT_ACCEL_DEG)
    logger.success("Pipeline complete")


# ============================================================
# Main
# ============================================================

def main():
    robot   = UniversalRobotsUR10E()
    gripper = OnRobotRG6()

    robot_connected   = False
    gripper_connected = False

    try:
        logger.info(f"Connecting to UR10e at {ROBOT_IP}")
        robot.connect(ROBOT_IP)
        robot_connected = True
        logger.success("Robot connected")

        logger.info(f"Connecting to OnRobot RG6 at {GRIPPER_IP} via {GRIPPER_PROTOCOL}")
        gripper.connect(ip=GRIPPER_IP, protocol=GRIPPER_PROTOCOL)
        gripper_connected = True
        logger.success("Gripper connected")

        run_pipeline(robot, gripper)

    except Exception as e:
        logger.exception(f"Pipeline failed: {type(e).__name__}: {e}")
    finally:
        if gripper_connected:
            try:
                logger.info("Disconnecting gripper")
                gripper.disconnect()
            except Exception as e:
                logger.warning(f"Gripper disconnect failed: {e}")
        if robot_connected:
            try:
                logger.info("Disconnecting robot")
                robot.disconnect()
            except Exception as e:
                logger.warning(f"Robot disconnect failed: {e}")
        logger.info("Cleanup complete")


if __name__ == "__main__":
    main()